diff --git a/config/default.rb b/config/default.rb index 3f3003658..e4002fb03 100644 --- a/config/default.rb +++ b/config/default.rb @@ -13,6 +13,8 @@ Vagrant.configure("2") do |config| config.vm.usable_port_range = (2200..2250) config.vm.box_url = nil config.vm.base_mac = nil + config.vm.graceful_halt_retry_count = 60 + config.vm.graceful_halt_retry_interval = 1 config.vm.guest = :linux # Share SSH locally by default diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index e9606e8a8..f73a0bbf8 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -14,6 +14,7 @@ module Vagrant autoload :Confirm, "vagrant/action/builtin/confirm" autoload :ConfigValidate, "vagrant/action/builtin/config_validate" autoload :EnvSet, "vagrant/action/builtin/env_set" + autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt" autoload :Provision, "vagrant/action/builtin/provision" autoload :SSHExec, "vagrant/action/builtin/ssh_exec" autoload :SSHRun, "vagrant/action/builtin/ssh_run" diff --git a/lib/vagrant/action/builtin/call.rb b/lib/vagrant/action/builtin/call.rb index 34cf59f30..e5edd3b6b 100644 --- a/lib/vagrant/action/builtin/call.rb +++ b/lib/vagrant/action/builtin/call.rb @@ -23,11 +23,12 @@ module Vagrant # can be a class, a lambda, or an object that responds to `call`. # @yield [result, builder] This block is expected to build on `builder` # which is the next middleware sequence that will be run. - def initialize(app, env, callable, &block) + def initialize(app, env, callable, *callable_args, &block) raise ArgumentError, "A block must be given to Call" if !block @app = app @callable = callable + @callable_args = callable_args @block = block @child_app = nil end @@ -35,8 +36,11 @@ module Vagrant def call(env) runner = Runner.new + # Build the callable that we'll run + callable = Builder.build(@callable, *@callable_args) + # Run our callable with our environment - new_env = runner.run(@callable, env) + new_env = runner.run(callable, env) # Build our new builder based on the result builder = Builder.new diff --git a/lib/vagrant/action/builtin/graceful_halt.rb b/lib/vagrant/action/builtin/graceful_halt.rb new file mode 100644 index 000000000..832695ff4 --- /dev/null +++ b/lib/vagrant/action/builtin/graceful_halt.rb @@ -0,0 +1,73 @@ +require "log4r" + +module Vagrant + module Action + module Builtin + # This middleware class will attempt to perform a graceful shutdown + # of the machine using the guest implementation. This middleware is + # compatible with the {Call} middleware so you can branch based on + # the result, which is true if the halt succeeded and false otherwise. + class GracefulHalt + # Note: Any of the arguments can be arrays as well. + # + # @param [Symbol] target_state The target state ID that means that + # the machine was properly shut down. + # @param [Symbol] source_state The source state ID that the machine + # must be in to be shut down. + def initialize(app, env, target_state, source_state=nil) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::graceful_halt") + @source_state = source_state + @target_state = target_state + end + + def call(env) + graceful = true + graceful = !env[:force_halt] if env.has_key?(:force_halt) + + # By default, we didn't succeed. + env[:result] = false + + if graceful && @source_state + @logger.info("Verifying source state of machine: #{@source_state.inspect}") + + # If we're not in the proper source state, then we don't + # attempt to halt the machine + current_state = env[:machine].state.id + if current_state != @source_state + @logger.info("Invalid source state, not halting: #{current_state}") + graceful = false + end + end + + # Only attempt to perform graceful shutdown under certain cases + # checked above. + if graceful + env[:ui].info I18n.t("vagrant.actions.vm.halt.graceful") + env[:machine].guest.halt + + @logger.debug("Waiting for target graceful halt state: #{@target_state}") + count = 0 + while env[:machine].state.id != @target_state + count += 1 + return if count >= env[:machine].config.vm.graceful_halt_retry_count + sleep env[:machine].config.vm.graceful_halt_retry_interval + end + + # The result of this matters on whether we reached our + # proper target state or not. + env[:result] = env[:machine].state.id == @target_state + + if env[:result] + @logger.info("Gracefully halted.") + else + @logger.info("Graceful halt failed.") + end + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/guests/freebsd/guest.rb b/plugins/guests/freebsd/guest.rb index 084197762..204dc5483 100644 --- a/plugins/guests/freebsd/guest.rb +++ b/plugins/guests/freebsd/guest.rb @@ -13,17 +13,6 @@ module VagrantPlugins def halt vm.channel.sudo("shutdown -p now") - - # Wait until the VM's state is actually powered off. If this doesn't - # occur within a reasonable amount of time (15 seconds by default), - # then simply return and allow Vagrant to kill the machine. - count = 0 - while vm.state != :poweroff - count += 1 - - return if count >= vm.config.freebsd.halt_timeout - sleep vm.config.freebsd.halt_check_interval - end end # TODO: vboxsf is currently unsupported in FreeBSD, if you are able to diff --git a/plugins/guests/linux/guest.rb b/plugins/guests/linux/guest.rb index b586f93fe..cb53875ac 100644 --- a/plugins/guests/linux/guest.rb +++ b/plugins/guests/linux/guest.rb @@ -35,17 +35,6 @@ module VagrantPlugins def halt @vm.communicate.sudo("shutdown -h now") - - # Wait until the VM's state is actually powered off. If this doesn't - # occur within a reasonable amount of time (15 seconds by default), - # then simply return and allow Vagrant to kill the machine. - count = 0 - while @vm.state != :poweroff - count += 1 - - return if count >= @vm.config.linux.halt_timeout - sleep @vm.config.linux.halt_check_interval - end end def mount_shared_folder(name, guestpath, options) diff --git a/plugins/guests/openbsd/guest.rb b/plugins/guests/openbsd/guest.rb index 5fb8af200..a43ed6f10 100644 --- a/plugins/guests/openbsd/guest.rb +++ b/plugins/guests/openbsd/guest.rb @@ -7,17 +7,6 @@ module VagrantPlugins class Guest < VagrantPlugins::GuestLinux::Guest def halt vm.channel.sudo("shutdown -p -h now") - - # Wait until the VM's state is actually powered off. If this doesn't - # occur within a reasonable amount of time then simply return which - # will cause Vagrant to force kill the machine. - count = 0 - while vm.state != :poweroff - count += 1 - - return if count >= 30 - sleep 1 - end end end end diff --git a/plugins/guests/solaris/guest.rb b/plugins/guests/solaris/guest.rb index e85a731c5..a8497bb14 100644 --- a/plugins/guests/solaris/guest.rb +++ b/plugins/guests/solaris/guest.rb @@ -45,35 +45,7 @@ module VagrantPlugins # # does not exist in /etc/user_attr. TODO def halt - # Wait until the VM's state is actually powered off. If this doesn't - # occur within a reasonable amount of time (15 seconds by default), - # then simply return and allow Vagrant to kill the machine. - count = 0 - last_error = nil - while vm.state != :poweroff - begin - vm.channel.execute("#{vm.config.solaris.suexec_cmd} /usr/sbin/poweroff") - rescue IOError => e - # Save the last error; if it's not shutdown in a reasonable amount - # of attempts we will re-raise the error so it's not hidden for - # all time - last_error = e - end - - count += 1 - if count >= vm.config.solaris.halt_timeout - # Check for last error and re-raise it - if last_error != nil - raise last_error - else - # Otherwise, just return - return - end - end - - # Still opportunities remaining; sleep and loop - sleep vm.config.solaris.halt_check_interval - end # while + vm.channel.execute("#{vm.config.solaris.suexec_cmd} /usr/sbin/poweroff") end def mount_shared_folder(name, guestpath, options) diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 3df3c5ca1..0ce31b54c 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -15,6 +15,8 @@ module VagrantPlugins attr_accessor :base_mac attr_accessor :box attr_accessor :box_url + attr_accessor :graceful_halt_retry_count + attr_accessor :graceful_halt_retry_interval attr_accessor :guest attr_accessor :host_name attr_accessor :usable_port_range @@ -25,10 +27,12 @@ module VagrantPlugins attr_reader :provisioners def initialize - @forwarded_ports = [] - @shared_folders = {} - @networks = [] - @provisioners = [] + @forwarded_ports = [] + @graceful_halt_retry_count = UNSET_VALUE + @graceful_halt_retry_interval = UNSET_VALUE + @shared_folders = {} + @networks = [] + @provisioners = [] # The providers hash defaults any key to a provider object @providers = Hash.new do |hash, key| diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 9a0908dd3..756c9a8cf 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -23,8 +23,8 @@ module VagrantPlugins autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) autoload :Export, File.expand_path("../action/export", __FILE__) + autoload :ForcedHalt, File.expand_path("../action/forced_halt", __FILE__) autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) - autoload :Halt, File.expand_path("../action/halt", __FILE__) autoload :HostName, File.expand_path("../action/host_name", __FILE__) autoload :Import, File.expand_path("../action/import", __FILE__) autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) @@ -110,7 +110,11 @@ module VagrantPlugins if env[:result] b2.use CheckAccessible b2.use DiscardState - b2.use Halt + b2.use Call, GracefulHalt, :poweroff, :running do |env2, b3| + if !env[:result] + b3.use ForcedHalt + end + end else b2.use MessageNotCreated end diff --git a/plugins/providers/virtualbox/action/forced_halt.rb b/plugins/providers/virtualbox/action/forced_halt.rb new file mode 100644 index 000000000..8fc20782c --- /dev/null +++ b/plugins/providers/virtualbox/action/forced_halt.rb @@ -0,0 +1,25 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ForcedHalt + def initialize(app, env) + @app = app + end + + def call(env) + current_state = env[:machine].state.id + if current_state == :running || current_state == :gurumeditation + env[:ui].info I18n.t("vagrant.actions.vm.halt.force") + env[:machine].provider.driver.halt + end + + # Sleep for a second to verify that the VM properly + # cleans itself up. Silly VirtualBox. + sleep 1 if !env["vagrant.test"] + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/halt.rb b/plugins/providers/virtualbox/action/halt.rb deleted file mode 100644 index d51990910..000000000 --- a/plugins/providers/virtualbox/action/halt.rb +++ /dev/null @@ -1,35 +0,0 @@ -module VagrantPlugins - module ProviderVirtualBox - module Action - class Halt - def initialize(app, env) - @app = app - end - - def call(env) - current_state = env[:machine].provider.state.id - if current_state == :running || current_state == :gurumeditation - # If the VM is running and we're not forcing, we can - # attempt a graceful shutdown - if current_state == :running && !env[:force] - env[:ui].info I18n.t("vagrant.actions.vm.halt.graceful") - env[:machine].guest.halt - end - - # If we're not powered off now, then force it - if env[:machine].provider.state.id != :poweroff - env[:ui].info I18n.t("vagrant.actions.vm.halt.force") - env[:machine].provider.driver.halt - end - - # Sleep for a second to verify that the VM properly - # cleans itself up - sleep 1 if !env["vagrant.test"] - end - - @app.call(env) - end - end - end - end -end diff --git a/test/unit/vagrant/action/builtin/call_test.rb b/test/unit/vagrant/action/builtin/call_test.rb index 58eb4969b..95dbd5e37 100644 --- a/test/unit/vagrant/action/builtin/call_test.rb +++ b/test/unit/vagrant/action/builtin/call_test.rb @@ -53,6 +53,26 @@ describe Vagrant::Action::Builtin::Call do received.should == :bar end + it "should instantiate the callable with the extra args" do + env = {} + + callable = Class.new do + def initialize(app, env, arg) + env[:arg] = arg + end + + def call(env); end + end + + result = nil + instance = described_class.new(app, env, callable, :foo) do |inner_env, _builder| + result = inner_env[:arg] + end + instance.call(env) + + result.should == :foo + end + it "should call the recover method for the sequence in an error" do # Basic variables callable = lambda { |env| } diff --git a/test/unit/vagrant/action/builtin/graceful_halt_test.rb b/test/unit/vagrant/action/builtin/graceful_halt_test.rb new file mode 100644 index 000000000..244bf2d17 --- /dev/null +++ b/test/unit/vagrant/action/builtin/graceful_halt_test.rb @@ -0,0 +1,61 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::GracefulHalt do + let(:app) { lambda { |env| } } + let(:env) { { :machine => machine, :ui => ui } } + let(:machine) do + result = double("machine") + result.stub(:config).and_return(machine_config) + result.stub(:guest).and_return(machine_guest) + result.stub(:state).and_return(machine_state) + result + end + let(:machine_config) do + double("machine_config").tap do |top_config| + vm_config = double("machien_vm_config") + vm_config.stub(:graceful_halt_retry_count => 2) + vm_config.stub(:graceful_halt_retry_interval => 0) + top_config.stub(:vm => vm_config) + end + end + let(:machine_guest) { double("machine_guest") } + let(:machine_state) do + double("machine_state").tap do |result| + result.stub(:id).and_return(:unknown) + end + end + let(:target_state) { :target } + let(:ui) do + double("ui").tap do |result| + result.stub(:info) + end + end + + it "should do nothing if force is specified" do + env[:force_halt] = true + + machine_guest.should_not_receive(:halt) + + described_class.new(app, env, target_state).call(env) + + env[:result].should == false + end + + it "should do nothing if there is an invalid source state" do + machine_state.stub(:id).and_return(:invalid_source) + machine_guest.should_not_receive(:halt) + + described_class.new(app, env, target_state, :target_source).call(env) + + env[:result].should == false + end + + it "should gracefully halt and wait for the target state" do + machine_guest.should_receive(:halt).once + machine_state.stub(:id).and_return(target_state) + + described_class.new(app, env, target_state).call(env) + + env[:result].should == true + end +end