diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 8995f6505..b4a609aa1 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -740,6 +740,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 668cbe0ef..b0051ce3d 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__) @@ -313,7 +315,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..090b769c4 --- /dev/null +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -0,0 +1,62 @@ +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[: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. + @app.call(env) + end + + env[:ui].info I18n.t("vagrant.actions.vm.clone.importing", name: env[:machine].box.name) + + #TODO - prevent concurrent creation of master vms for the same box. + + # 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 + + # If we got interrupted, then the import could have been + # interrupted and its not a big deal. Just return out. + return if env[:interrupted] + + # 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 aed1c692c..7a0e08d08 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 1da74d5d3..5f0700c32 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -79,8 +79,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 cc700c0fa..76ccdcd6a 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.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], @@ -62,6 +71,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 @@ -156,6 +169,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 @@ -231,10 +251,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 4be7a92e7..4cbf5a0a3 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1482,6 +1482,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: