Merge branch 'mpoeter-VB-linked-clone-support'

This commit is contained in:
Mitchell Hashimoto 2015-10-06 13:50:41 -04:00
commit b8c7d16878
11 changed files with 225 additions and 10 deletions

View File

@ -526,7 +526,11 @@ module Vagrant
if name != "dotlock" if name != "dotlock"
lock("dotlock", retry: true) do lock("dotlock", retry: true) do
f.close 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
end end

View File

@ -784,6 +784,14 @@ module Vagrant
error_key(:boot_timeout) error_key(:boot_timeout)
end 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 class VMCustomizationFailed < VagrantError
error_key(:failure, "vagrant.actions.vm.customize") error_key(:failure, "vagrant.actions.vm.customize")
end end

View File

@ -12,6 +12,7 @@ module VagrantPlugins
autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__)
autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__)
autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __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 :Created, File.expand_path("../action/created", __FILE__)
autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Customize, File.expand_path("../action/customize", __FILE__)
autoload :Destroy, File.expand_path("../action/destroy", __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 :ForcedHalt, File.expand_path("../action/forced_halt", __FILE__)
autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__)
autoload :Import, File.expand_path("../action/import", __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 :IsPaused, File.expand_path("../action/is_paused", __FILE__)
autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) autoload :IsRunning, File.expand_path("../action/is_running", __FILE__)
autoload :IsSaved, File.expand_path("../action/is_saved", __FILE__) autoload :IsSaved, File.expand_path("../action/is_saved", __FILE__)
@ -325,7 +327,13 @@ module VagrantPlugins
if !env[:result] if !env[:result]
b2.use CheckAccessible b2.use CheckAccessible
b2.use Customize, "pre-import" 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 b2.use MatchMACAddress
end end
end end

View File

@ -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

View File

@ -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

View File

@ -32,6 +32,12 @@ module VagrantPlugins
# @return [Boolean] # @return [Boolean]
attr_accessor :gui 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 # This should be set to the name of the machine in the VirtualBox
# GUI. # GUI.
# #
@ -59,6 +65,7 @@ module VagrantPlugins
@name = UNSET_VALUE @name = UNSET_VALUE
@network_adapters = {} @network_adapters = {}
@gui = UNSET_VALUE @gui = UNSET_VALUE
@use_linked_clone = UNSET_VALUE
# We require that network adapter 1 is a NAT device. # We require that network adapter 1 is a NAT device.
network_adapter(1, :nat) network_adapter(1, :nat)
@ -136,6 +143,9 @@ module VagrantPlugins
# Default is to not show a GUI # Default is to not show a GUI
@gui = false if @gui == UNSET_VALUE @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 # The default name is just nothing, and we default it
@name = nil if @name == UNSET_VALUE @name = nil if @name == UNSET_VALUE
end end

View File

@ -84,8 +84,10 @@ module VagrantPlugins
def_delegators :@driver, :clear_forwarded_ports, def_delegators :@driver, :clear_forwarded_ports,
:clear_shared_folders, :clear_shared_folders,
:clonevm,
:create_dhcp_server, :create_dhcp_server,
:create_host_only_network, :create_host_only_network,
:create_snapshot,
:delete, :delete,
:delete_unused_host_only_networks, :delete_unused_host_only_networks,
:discard_saved_state, :discard_saved_state,

View File

@ -35,6 +35,15 @@ module VagrantPlugins
end end
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) def create_dhcp_server(network, options)
execute("dhcpserver", "add", "--ifname", network, execute("dhcpserver", "add", "--ifname", network,
"--ip", options[:dhcp_ip], "--ip", options[:dhcp_ip],
@ -77,6 +86,10 @@ module VagrantPlugins
"--ipv6", interface[:ipv6]) "--ipv6", interface[:ipv6])
end end
def create_snapshot(machine_id, snapshot_name)
execute("snapshot", machine_id, "take", snapshot_name)
end
def delete def delete
execute("unregistervm", @uuid, "--delete") execute("unregistervm", @uuid, "--delete")
end end
@ -171,6 +184,13 @@ module VagrantPlugins
execute("modifyvm", @uuid, *args) if !args.empty? execute("modifyvm", @uuid, *args) if !args.empty?
end 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 def halt
execute("controlvm", @uuid, "poweroff") execute("controlvm", @uuid, "poweroff")
end end
@ -246,10 +266,7 @@ module VagrantPlugins
end end
end end
output = execute("list", "vms", retryable: true) return get_machine_id specified_name
match = /^"#{Regexp.escape(specified_name)}" \{(.+?)\}$/.match(output)
return match[1].to_s if match
nil
end end
def max_network_adapters def max_network_adapters

View File

@ -34,6 +34,15 @@ module VagrantPlugins
end end
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) def create_dhcp_server(network, options)
execute("dhcpserver", "add", "--ifname", network, execute("dhcpserver", "add", "--ifname", network,
"--ip", options[:dhcp_ip], "--ip", options[:dhcp_ip],
@ -73,6 +82,10 @@ module VagrantPlugins
} }
end end
def create_snapshot(machine_id, snapshot_name)
execute("snapshot", machine_id, "take", snapshot_name)
end
def delete def delete
execute("unregistervm", @uuid, "--delete") execute("unregistervm", @uuid, "--delete")
end end
@ -167,6 +180,13 @@ module VagrantPlugins
execute("modifyvm", @uuid, *args) if !args.empty? execute("modifyvm", @uuid, *args) if !args.empty?
end 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 def halt
execute("controlvm", @uuid, "poweroff") execute("controlvm", @uuid, "poweroff")
end end
@ -242,10 +262,7 @@ module VagrantPlugins
end end
end end
output = execute("list", "vms", retryable: true) return get_machine_id specified_name
match = /^"#{Regexp.escape(specified_name)}" \{(.+?)\}$/.match(output)
return match[1].to_s if match
nil
end end
def max_network_adapters def max_network_adapters

View File

@ -1618,6 +1618,14 @@ en:
deleting: Clearing any previously set network interfaces... deleting: Clearing any previously set network interfaces...
clear_shared_folders: clear_shared_folders:
deleting: Cleaning previously set 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: customize:
failure: |- failure: |-
A customization command failed: A customization command failed:

View File

@ -36,6 +36,30 @@ config.vm.provider "virtualbox" do |v|
end 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
```
<div class="alert alert-info">
<strong>Note:</strong> 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.
</div>
## VBoxManage Customizations ## VBoxManage Customizations
[VBoxManage](http://www.virtualbox.org/manual/ch08.html) is a utility that can [VBoxManage](http://www.virtualbox.org/manual/ch08.html) is a utility that can