Merge pull request #6378 from mitchellh/f-clone

Clone another Vagrant environment
This commit is contained in:
Mitchell Hashimoto 2015-10-08 13:19:27 -04:00
commit d8b8eb7a6d
14 changed files with 234 additions and 101 deletions

View File

@ -9,6 +9,7 @@ require 'log4r'
require 'vagrant/util/file_mode' require 'vagrant/util/file_mode'
require 'vagrant/util/platform' require 'vagrant/util/platform'
require "vagrant/util/silence_warnings"
require "vagrant/vagrantfile" require "vagrant/vagrantfile"
require "vagrant/version" require "vagrant/version"
@ -413,6 +414,27 @@ module Vagrant
@config_loader @config_loader
end end
# Loads another environment for the given Vagrantfile, sharing as much
# useful state from this Environment as possible (such as UI and paths).
# Any initialization options can be overidden using the opts hash.
#
# @param [String] vagrantfile Path to a Vagrantfile
# @return [Environment]
def environment(vagrantfile, **opts)
path = File.expand_path(vagrantfile, root_path)
file = File.basename(path)
path = File.dirname(path)
Util::SilenceWarnings.silence! do
Environment.new({
cwd: path,
home_path: home_path,
ui_class: ui_class,
vagrantfile_name: file,
}.merge(opts))
end
end
# This defines a hook point where plugin action hooks that are registered # This defines a hook point where plugin action hooks that are registered
# against the given name will be run in the context of this environment. # against the given name will be run in the context of this environment.
# #

View File

@ -288,6 +288,14 @@ module Vagrant
error_key(:cli_invalid_options) error_key(:cli_invalid_options)
end end
class CloneNotFound < VagrantError
error_key(:clone_not_found)
end
class CloneMachineNotFound < VagrantError
error_key(:clone_machine_not_found)
end
class CommandUnavailable < VagrantError class CommandUnavailable < VagrantError
error_key(:command_unavailable) error_key(:command_unavailable)
end end

View File

@ -37,6 +37,9 @@ module VagrantPlugins
attr_accessor :usable_port_range attr_accessor :usable_port_range
attr_reader :provisioners attr_reader :provisioners
# This is an experimental feature that isn't public yet.
attr_accessor :clone
def initialize def initialize
@logger = Log4r::Logger.new("vagrant::config::vm") @logger = Log4r::Logger.new("vagrant::config::vm")
@ -54,6 +57,7 @@ module VagrantPlugins
@box_download_location_trusted = UNSET_VALUE @box_download_location_trusted = UNSET_VALUE
@box_url = UNSET_VALUE @box_url = UNSET_VALUE
@box_version = UNSET_VALUE @box_version = UNSET_VALUE
@clone = UNSET_VALUE
@communicator = UNSET_VALUE @communicator = UNSET_VALUE
@graceful_halt_timeout = UNSET_VALUE @graceful_halt_timeout = UNSET_VALUE
@guest = UNSET_VALUE @guest = UNSET_VALUE
@ -367,6 +371,7 @@ module VagrantPlugins
@box_download_location_trusted = false if @box_download_location_trusted == UNSET_VALUE @box_download_location_trusted = false if @box_download_location_trusted == UNSET_VALUE
@box_url = nil if @box_url == UNSET_VALUE @box_url = nil if @box_url == UNSET_VALUE
@box_version = nil if @box_version == UNSET_VALUE @box_version = nil if @box_version == UNSET_VALUE
@clone = nil if @clone == UNSET_VALUE
@communicator = nil if @communicator == UNSET_VALUE @communicator = nil if @communicator == UNSET_VALUE
@graceful_halt_timeout = 60 if @graceful_halt_timeout == UNSET_VALUE @graceful_halt_timeout = 60 if @graceful_halt_timeout == UNSET_VALUE
@guest = nil if @guest == UNSET_VALUE @guest = nil if @guest == UNSET_VALUE
@ -554,10 +559,14 @@ module VagrantPlugins
def validate(machine) def validate(machine)
errors = _detected_errors errors = _detected_errors
if !box && !machine.provider_options[:box_optional] if !box && !clone && !machine.provider_options[:box_optional]
errors << I18n.t("vagrant.config.vm.box_missing") errors << I18n.t("vagrant.config.vm.box_missing")
end end
if box && clone
errors << I18n.t("vagrant.config.vm.clone_and_box")
end
errors << I18n.t("vagrant.config.vm.hostname_invalid_characters") if \ errors << I18n.t("vagrant.config.vm.hostname_invalid_characters") if \
@hostname && @hostname !~ /^[a-z0-9][-.a-z0-9]*$/i @hostname && @hostname !~ /^[a-z0-9][-.a-z0-9]*$/i

View File

@ -12,7 +12,6 @@ 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__)
@ -35,6 +34,8 @@ module VagrantPlugins
autoload :NetworkFixIPv6, File.expand_path("../action/network_fix_ipv6", __FILE__) autoload :NetworkFixIPv6, File.expand_path("../action/network_fix_ipv6", __FILE__)
autoload :Package, File.expand_path("../action/package", __FILE__) autoload :Package, File.expand_path("../action/package", __FILE__)
autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__)
autoload :PrepareClone, File.expand_path("../action/prepare_clone", __FILE__)
autoload :PrepareCloneSnapshot, File.expand_path("../action/prepare_clone_snapshot", __FILE__)
autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__) autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__)
autoload :PrepareNFSValidIds, File.expand_path("../action/prepare_nfs_valid_ids", __FILE__) autoload :PrepareNFSValidIds, File.expand_path("../action/prepare_nfs_valid_ids", __FILE__)
autoload :PrepareForwardedPortCollisionParams, File.expand_path("../action/prepare_forwarded_port_collision_params", __FILE__) autoload :PrepareForwardedPortCollisionParams, File.expand_path("../action/prepare_forwarded_port_collision_params", __FILE__)
@ -384,16 +385,21 @@ module VagrantPlugins
b2.use CheckAccessible b2.use CheckAccessible
b2.use Customize, "pre-import" b2.use Customize, "pre-import"
if env[:machine].provider_config.linked_clone if env[:machine].config.vm.clone
# We are cloning from another Vagrant environment
b2.use PrepareClone
elsif env[:machine].provider_config.linked_clone
# We are cloning from the box
b2.use ImportMaster b2.use ImportMaster
b2.use CreateClone
else
b2.use Import
end end
b2.use PrepareCloneSnapshot
b2.use Import
b2.use DiscardState
b2.use MatchMACAddress b2.use MatchMACAddress
end end
end end
b.use action_start b.use action_start
end end
end end

View File

@ -1,61 +0,0 @@
require "log4r"
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]}'")
# Get the snapshot to base the linked clone on. This defaults
# to "base" which is automatically setup with linked clones.
snapshot = "base"
if env[:machine].provider_config.linked_clone_snapshot
snapshot = env[:machine].provider_config.linked_clone_snapshot
end
# Do the actual clone
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, snapshot) 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

@ -7,6 +7,44 @@ module VagrantPlugins
end end
def call(env) def call(env)
if env[:clone_id]
clone(env)
else
import(env)
end
end
def clone(env)
# Do the actual clone
env[:ui].info I18n.t("vagrant.actions.vm.clone.creating")
env[:machine].id = env[:machine].provider.driver.clonevm(
env[:clone_id], env[:clone_snapshot]) 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
# Copy the SSH key from the clone machine if we can
if env[:clone_machine]
key_path = env[:clone_machine].data_dir.join("private_key")
if key_path.file?
FileUtils.cp(
key_path,
env[:machine].data_dir.join("private_key"))
end
end
# Continue
@app.call(env)
end
def import(env)
env[:ui].info I18n.t("vagrant.actions.vm.import.importing", env[:ui].info I18n.t("vagrant.actions.vm.import.importing",
name: env[:machine].box.name) name: env[:machine].box.name)

View File

@ -37,14 +37,14 @@ module VagrantPlugins
master_id_file = env[:machine].box.directory.join("master_id") master_id_file = env[:machine].box.directory.join("master_id")
# Read the master ID if we have it in the file. # Read the master ID if we have it in the file.
env[:master_id] = master_id_file.read.chomp if master_id_file.file? env[:clone_id] = master_id_file.read.chomp if master_id_file.file?
# If we have the ID and the VM exists already, then we # If we have the ID and the VM exists already, then we
# have nothing to do. Success! # have nothing to do. Success!
if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) if env[:clone_id] && env[:machine].provider.driver.vm_exists?(env[:clone_id])
@logger.info( @logger.info(
"Master VM for '#{env[:machine].box.name}' already exists " + "Master VM for '#{env[:machine].box.name}' already exists " +
" (id=#{env[:master_id]}) - skipping import step.") " (id=#{env[:clone_id]}) - skipping import step.")
return return
end end
@ -53,27 +53,15 @@ module VagrantPlugins
# Import the virtual machine # Import the virtual machine
import_env = env[:action_runner].run(Import, env.dup.merge(skip_machine: true)) import_env = env[:action_runner].run(Import, env.dup.merge(skip_machine: true))
env[:master_id] = import_env[:machine_id] env[:clone_id] = import_env[:machine_id]
@logger.info( @logger.info(
"Imported box #{env[:machine].box.name} as master vm " + "Imported box #{env[:machine].box.name} as master vm " +
"with id #{env[:master_id]}") "with id #{env[:clone_id]}")
if !env[:machine].provider_config.linked_clone_snapshot @logger.debug("Writing id of master VM '#{env[:clone_id]}' to #{master_id_file}")
snapshots = env[:machine].provider.driver.list_snapshots(env[:master_id])
if !snapshots.include?("base")
@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
end
end
@logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}")
master_id_file.open("w+") do |f| master_id_file.open("w+") do |f|
f.write(env[:master_id]) f.write(env[:clone_id])
end end
end end
end end

View File

@ -7,6 +7,9 @@ module VagrantPlugins
end end
def call(env) def call(env)
# If we cloned, we don't need a base mac, it is already set!
return @app.call(env) if env[:machine].config.vm.clone
raise Vagrant::Errors::VMBaseMacNotSpecified if !env[:machine].config.vm.base_mac raise Vagrant::Errors::VMBaseMacNotSpecified if !env[:machine].config.vm.base_mac
# Create the proc which we want to use to modify the virtual machine # Create the proc which we want to use to modify the virtual machine

View File

@ -0,0 +1,33 @@
require "log4r"
module VagrantPlugins
module ProviderVirtualBox
module Action
class PrepareClone
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant::action::vm::prepare_clone")
end
def call(env)
# We need to get the machine ID from this Vagrant environment
clone_env = env[:machine].env.environment(
env[:machine].config.vm.clone)
raise Vagrant::Errors::CloneNotFound if !clone_env.root_path
# Get the machine itself
clone_machine = clone_env.machine(
clone_env.primary_machine_name, env[:machine].provider_name)
raise Vagrant::Errors::CloneMachineNotFound if !clone_machine.id
# Set the ID of the master so we know what to clone from
env[:clone_id] = clone_machine.id
env[:clone_machine] = clone_machine
# Continue
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,65 @@
require "log4r"
require "digest/md5"
module VagrantPlugins
module ProviderVirtualBox
module Action
class PrepareCloneSnapshot
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant::action::vm::prepare_clone")
end
def call(env)
if !env[:clone_id]
@logger.info("no clone master, not preparing clone snapshot")
return @app.call(env)
end
# If we're not doing a linked clone, snapshots don't matter
if !env[:machine].provider_config.linked_clone
return @app.call(env)
end
# We lock so that we don't snapshot in parallel
lock_key = Digest::MD5.hexdigest("#{env[:clone_id]}-snapshot")
env[:machine].env.lock(lock_key, retry: true) do
prepare_snapshot(env)
end
# Continue
@app.call(env)
end
protected
def prepare_snapshot(env)
name = env[:machine].provider_config.linked_clone_snapshot
name_set = !!name
name = "base" if !name
env[:clone_snapshot] = name
# Get the snapshots. We're done if it already exists
snapshots = env[:machine].provider.driver.list_snapshots(env[:clone_id])
if snapshots.include?(name)
@logger.info("clone snapshot already exists, doing nothing")
return
end
# If they asked for a specific snapshot, it is an error
if name_set
# TODO: Error
end
@logger.info("Creating base snapshot for master VM.")
env[:machine].provider.driver.create_snapshot(
env[:clone_id], name) do |progress|
env[:ui].clear_line
env[:ui].report_progress(progress, 100, false)
end
end
end
end
end
end

View File

@ -35,13 +35,15 @@ module VagrantPlugins
end end
end end
def clonevm(master_id, box_name, snapshot_name) def clonevm(master_id, snapshot_name)
@logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") machine_name = "temp_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}"
args = ["--register", "--name", machine_name]
if snapshot_name
args += ["--snapshot", snapshot_name, "--options", "link"]
end
machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" execute("clonevm", master_id, *args)
execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) return get_machine_id(machine_name)
return get_machine_id machine_name
end end
def create_dhcp_server(network, options) def create_dhcp_server(network, options)

View File

@ -34,13 +34,15 @@ module VagrantPlugins
end end
end end
def clonevm(master_id, box_name, snapshot_name) def clonevm(master_id, snapshot_name)
@logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") machine_name = "temp_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}"
args = ["--register", "--name", machine_name]
if snapshot_name
args += ["--snapshot", snapshot_name, "--options", "link"]
end
machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" execute("clonevm", master_id, *args)
execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) return get_machine_id(machine_name)
return get_machine_id machine_name
end end
def create_dhcp_server(network, options) def create_dhcp_server(network, options)

View File

@ -650,6 +650,17 @@ en:
available below. available below.
%{help} %{help}
clone_not_found: |-
The specified Vagrantfile to clone from was not found. Please verify
the `config.vm.clone` setting points to a valid Vagrantfile.
clone_machine_not_found: |-
The clone environment hasn't been created yet. To clone from
another Vagrantfile, it must already be created with `vagrant up`.
It doesn't need to be running.
Additionally, the created environment must be started with a provider
matching this provider. For example, if you're using VirtualBox,
the clone environment must also be using VirtualBox.
command_unavailable: |- command_unavailable: |-
The executable '%{file}' Vagrant is trying to run was not The executable '%{file}' Vagrant is trying to run was not
found in the PATH variable. This is an error. Please verify found in the PATH variable. This is an error. Please verify
@ -1404,6 +1415,7 @@ en:
box_download_checksum_notblank: |- box_download_checksum_notblank: |-
Checksum specified but must also specify "box_download_checksum_type" Checksum specified but must also specify "box_download_checksum_type"
box_missing: "A box must be specified." box_missing: "A box must be specified."
clone_and_box: "Only one of clone or box can be specified."
hostname_invalid_characters: |- hostname_invalid_characters: |-
The hostname set for the VM should only contain letters, numbers, The hostname set for the VM should only contain letters, numbers,
hyphens or dots. It cannot start with a hyphen or dot. hyphens or dots. It cannot start with a hyphen or dot.
@ -1635,7 +1647,7 @@ en:
This is a one time operation. Once the master VM is prepared, This is a one time operation. Once the master VM is prepared,
it will be used as a base for linked clones, making the creation it will be used as a base for linked clones, making the creation
of new VMs take milliseconds on a modern system. of new VMs take milliseconds on a modern system.
creating: Creating linked clone... creating: Cloning VM...
failure: Creation of the linked clone failed. failure: Creation of the linked clone failed.
create_master: create_master:
failure: |- failure: |-

View File

@ -65,6 +65,12 @@ describe VagrantPlugins::Kernel_V2::VMConfig do
subject.finalize! subject.finalize!
assert_valid assert_valid
end end
it "is invalid if clone is set" do
subject.clone = "foo"
subject.finalize!
assert_invalid
end
end end
context "#box_check_update" do context "#box_check_update" do