Merge branch 'graceful-halt-builtin'
This introduces a new middleware builtin: GracefulHalt. This should be used to gracefully shut down the machine. It removes any concept of timeouts and such out of the guests.
This commit is contained in:
commit
030fd30dad
|
@ -13,6 +13,8 @@ Vagrant.configure("2") do |config|
|
||||||
config.vm.usable_port_range = (2200..2250)
|
config.vm.usable_port_range = (2200..2250)
|
||||||
config.vm.box_url = nil
|
config.vm.box_url = nil
|
||||||
config.vm.base_mac = nil
|
config.vm.base_mac = nil
|
||||||
|
config.vm.graceful_halt_retry_count = 60
|
||||||
|
config.vm.graceful_halt_retry_interval = 1
|
||||||
config.vm.guest = :linux
|
config.vm.guest = :linux
|
||||||
|
|
||||||
# Share SSH locally by default
|
# Share SSH locally by default
|
||||||
|
|
|
@ -14,6 +14,7 @@ module Vagrant
|
||||||
autoload :Confirm, "vagrant/action/builtin/confirm"
|
autoload :Confirm, "vagrant/action/builtin/confirm"
|
||||||
autoload :ConfigValidate, "vagrant/action/builtin/config_validate"
|
autoload :ConfigValidate, "vagrant/action/builtin/config_validate"
|
||||||
autoload :EnvSet, "vagrant/action/builtin/env_set"
|
autoload :EnvSet, "vagrant/action/builtin/env_set"
|
||||||
|
autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt"
|
||||||
autoload :Provision, "vagrant/action/builtin/provision"
|
autoload :Provision, "vagrant/action/builtin/provision"
|
||||||
autoload :SSHExec, "vagrant/action/builtin/ssh_exec"
|
autoload :SSHExec, "vagrant/action/builtin/ssh_exec"
|
||||||
autoload :SSHRun, "vagrant/action/builtin/ssh_run"
|
autoload :SSHRun, "vagrant/action/builtin/ssh_run"
|
||||||
|
|
|
@ -23,11 +23,12 @@ module Vagrant
|
||||||
# can be a class, a lambda, or an object that responds to `call`.
|
# can be a class, a lambda, or an object that responds to `call`.
|
||||||
# @yield [result, builder] This block is expected to build on `builder`
|
# @yield [result, builder] This block is expected to build on `builder`
|
||||||
# which is the next middleware sequence that will be run.
|
# 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
|
raise ArgumentError, "A block must be given to Call" if !block
|
||||||
|
|
||||||
@app = app
|
@app = app
|
||||||
@callable = callable
|
@callable = callable
|
||||||
|
@callable_args = callable_args
|
||||||
@block = block
|
@block = block
|
||||||
@child_app = nil
|
@child_app = nil
|
||||||
end
|
end
|
||||||
|
@ -35,8 +36,11 @@ module Vagrant
|
||||||
def call(env)
|
def call(env)
|
||||||
runner = Runner.new
|
runner = Runner.new
|
||||||
|
|
||||||
|
# Build the callable that we'll run
|
||||||
|
callable = Builder.build(@callable, *@callable_args)
|
||||||
|
|
||||||
# Run our callable with our environment
|
# 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
|
# Build our new builder based on the result
|
||||||
builder = Builder.new
|
builder = Builder.new
|
||||||
|
|
|
@ -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
|
|
@ -13,17 +13,6 @@ module VagrantPlugins
|
||||||
|
|
||||||
def halt
|
def halt
|
||||||
vm.channel.sudo("shutdown -p now")
|
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
|
end
|
||||||
|
|
||||||
# TODO: vboxsf is currently unsupported in FreeBSD, if you are able to
|
# TODO: vboxsf is currently unsupported in FreeBSD, if you are able to
|
||||||
|
|
|
@ -35,17 +35,6 @@ module VagrantPlugins
|
||||||
|
|
||||||
def halt
|
def halt
|
||||||
@vm.communicate.sudo("shutdown -h now")
|
@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
|
end
|
||||||
|
|
||||||
def mount_shared_folder(name, guestpath, options)
|
def mount_shared_folder(name, guestpath, options)
|
||||||
|
|
|
@ -7,17 +7,6 @@ module VagrantPlugins
|
||||||
class Guest < VagrantPlugins::GuestLinux::Guest
|
class Guest < VagrantPlugins::GuestLinux::Guest
|
||||||
def halt
|
def halt
|
||||||
vm.channel.sudo("shutdown -p -h now")
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,35 +45,7 @@ module VagrantPlugins
|
||||||
#
|
#
|
||||||
# does not exist in /etc/user_attr. TODO
|
# does not exist in /etc/user_attr. TODO
|
||||||
def halt
|
def halt
|
||||||
# Wait until the VM's state is actually powered off. If this doesn't
|
vm.channel.execute("#{vm.config.solaris.suexec_cmd} /usr/sbin/poweroff")
|
||||||
# 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def mount_shared_folder(name, guestpath, options)
|
def mount_shared_folder(name, guestpath, options)
|
||||||
|
|
|
@ -15,6 +15,8 @@ module VagrantPlugins
|
||||||
attr_accessor :base_mac
|
attr_accessor :base_mac
|
||||||
attr_accessor :box
|
attr_accessor :box
|
||||||
attr_accessor :box_url
|
attr_accessor :box_url
|
||||||
|
attr_accessor :graceful_halt_retry_count
|
||||||
|
attr_accessor :graceful_halt_retry_interval
|
||||||
attr_accessor :guest
|
attr_accessor :guest
|
||||||
attr_accessor :host_name
|
attr_accessor :host_name
|
||||||
attr_accessor :usable_port_range
|
attr_accessor :usable_port_range
|
||||||
|
@ -25,10 +27,12 @@ module VagrantPlugins
|
||||||
attr_reader :provisioners
|
attr_reader :provisioners
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@forwarded_ports = []
|
@forwarded_ports = []
|
||||||
@shared_folders = {}
|
@graceful_halt_retry_count = UNSET_VALUE
|
||||||
@networks = []
|
@graceful_halt_retry_interval = UNSET_VALUE
|
||||||
@provisioners = []
|
@shared_folders = {}
|
||||||
|
@networks = []
|
||||||
|
@provisioners = []
|
||||||
|
|
||||||
# The providers hash defaults any key to a provider object
|
# The providers hash defaults any key to a provider object
|
||||||
@providers = Hash.new do |hash, key|
|
@providers = Hash.new do |hash, key|
|
||||||
|
|
|
@ -23,8 +23,8 @@ module VagrantPlugins
|
||||||
autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__)
|
autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__)
|
||||||
autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__)
|
autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__)
|
||||||
autoload :Export, File.expand_path("../action/export", __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 :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 :HostName, File.expand_path("../action/host_name", __FILE__)
|
||||||
autoload :Import, File.expand_path("../action/import", __FILE__)
|
autoload :Import, File.expand_path("../action/import", __FILE__)
|
||||||
autoload :IsRunning, File.expand_path("../action/is_running", __FILE__)
|
autoload :IsRunning, File.expand_path("../action/is_running", __FILE__)
|
||||||
|
@ -110,7 +110,11 @@ module VagrantPlugins
|
||||||
if env[:result]
|
if env[:result]
|
||||||
b2.use CheckAccessible
|
b2.use CheckAccessible
|
||||||
b2.use DiscardState
|
b2.use DiscardState
|
||||||
b2.use Halt
|
b2.use Call, GracefulHalt, :poweroff, :running do |env2, b3|
|
||||||
|
if !env[:result]
|
||||||
|
b3.use ForcedHalt
|
||||||
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
b2.use MessageNotCreated
|
b2.use MessageNotCreated
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
|
@ -53,6 +53,26 @@ describe Vagrant::Action::Builtin::Call do
|
||||||
received.should == :bar
|
received.should == :bar
|
||||||
end
|
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
|
it "should call the recover method for the sequence in an error" do
|
||||||
# Basic variables
|
# Basic variables
|
||||||
callable = lambda { |env| }
|
callable = lambda { |env| }
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue