diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index fcea6b69f..d4b8db1fd 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -526,7 +526,11 @@ module Vagrant if name != "dotlock" lock("dotlock", retry: true) do f.close - File.delete(lock_path) + 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 end end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index b705b362e..2ce133618 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -784,6 +784,14 @@ module Vagrant 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 0d8fc8e38..635907827 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -12,6 +12,7 @@ module VagrantPlugins 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__) @@ -21,6 +22,7 @@ module VagrantPlugins 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,7 +327,13 @@ module VagrantPlugins if !env[:result] b2.use CheckAccessible b2.use Customize, "pre-import" - b2.use Import + + if env[:machine].provider_config.use_linked_clone + b2.use ImportMaster + b2.use CreateClone + else + b2.use Import + end b2.use MatchMACAddress end end diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb new file mode 100644 index 000000000..e26b5721f --- /dev/null +++ b/plugins/providers/virtualbox/action/create_clone.rb @@ -0,0 +1,51 @@ +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_master.rb b/plugins/providers/virtualbox/action/import_master.rb new file mode 100644 index 000000000..61b0064eb --- /dev/null +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -0,0 +1,66 @@ +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 5bb1e754b..55b8356aa 100644 --- a/plugins/providers/virtualbox/config.rb +++ b/plugins/providers/virtualbox/config.rb @@ -32,6 +32,12 @@ module VagrantPlugins # @return [Boolean] attr_accessor :gui + # If set to `true`, then a linked clone is created from a master + # VM generated from the specified box. + # + # @return [Boolean] + attr_accessor :use_linked_clone + # This should be set to the name of the machine in the VirtualBox # GUI. # @@ -59,6 +65,7 @@ module VagrantPlugins @name = UNSET_VALUE @network_adapters = {} @gui = UNSET_VALUE + @use_linked_clone = UNSET_VALUE # We require that network adapter 1 is a NAT device. network_adapter(1, :nat) @@ -136,6 +143,9 @@ module VagrantPlugins # Default is to not show a GUI @gui = false if @gui == UNSET_VALUE + # Do not create linked clone by default + @use_linked_clone = false if @use_linked_clone == UNSET_VALUE + # The default name is just nothing, and we default it @name = nil if @name == UNSET_VALUE end diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 242372452..df52f0849 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -84,8 +84,10 @@ module VagrantPlugins 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, diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index 3b08d43cd..bc2c87478 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -35,6 +35,15 @@ module VagrantPlugins 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], @@ -77,6 +86,10 @@ module VagrantPlugins "--ipv6", interface[:ipv6]) end + def create_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "take", snapshot_name) + end + def delete execute("unregistervm", @uuid, "--delete") end @@ -171,6 +184,13 @@ module VagrantPlugins 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 @@ -246,10 +266,7 @@ module VagrantPlugins end end - output = execute("list", "vms", retryable: true) - match = /^"#{Regexp.escape(specified_name)}" \{(.+?)\}$/.match(output) - return match[1].to_s if match - nil + return get_machine_id specified_name end def max_network_adapters diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 3c84ad3c7..4bb839c1c 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -34,6 +34,15 @@ module VagrantPlugins 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], @@ -73,6 +82,10 @@ module VagrantPlugins } end + def create_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "take", snapshot_name) + end + def delete execute("unregistervm", @uuid, "--delete") end @@ -167,6 +180,13 @@ module VagrantPlugins 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 @@ -242,10 +262,7 @@ module VagrantPlugins end end - output = execute("list", "vms", retryable: true) - match = /^"#{Regexp.escape(specified_name)}" \{(.+?)\}$/.match(output) - return match[1].to_s if match - nil + return get_machine_id specified_name end def max_network_adapters diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 2401c1bf6..d0f311fd0 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1618,6 +1618,14 @@ 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/website/docs/source/v2/virtualbox/configuration.html.md b/website/docs/source/v2/virtualbox/configuration.html.md index 22cb2a6c5..5c908ab68 100644 --- a/website/docs/source/v2/virtualbox/configuration.html.md +++ b/website/docs/source/v2/virtualbox/configuration.html.md @@ -36,6 +36,30 @@ config.vm.provider "virtualbox" do |v| end ``` +## Linked Clones + +By default new machines are created by importing the base box. For large +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. + +```ruby +config.vm.provider "virtualbox" do |v| + v.use_linked_clone = true +end +``` + +