Merge branch 'VB-linked-clone-support' of https://github.com/mpoeter/vagrant into mpoeter-VB-linked-clone-support
This commit is contained in:
commit
d519d927fa
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue