diff --git a/lib/vagrant/actions/vm/halt.rb b/lib/vagrant/actions/vm/halt.rb index eec860f40..f5d241481 100644 --- a/lib/vagrant/actions/vm/halt.rb +++ b/lib/vagrant/actions/vm/halt.rb @@ -6,8 +6,12 @@ module Vagrant raise ActionException.new(:vm_not_running) unless @runner.vm.running? @runner.invoke_around_callback(:halt) do - logger.info "Forcing shutdown of VM..." - @runner.vm.stop + @runner.system.halt + + if @runner.vm.state(true) != :powered_off + logger.info "Forcing shutdown of VM..." + @runner.vm.stop + end end end end diff --git a/lib/vagrant/systems/base.rb b/lib/vagrant/systems/base.rb index 66d45df81..41c6e674a 100644 --- a/lib/vagrant/systems/base.rb +++ b/lib/vagrant/systems/base.rb @@ -27,6 +27,15 @@ module Vagrant @vm = vm end + # Halt the machine. This method should gracefully shut down the + # operating system. This method will cause `vagrant halt` and associated + # commands to _block_, meaning that if the machine doesn't halt + # in a reasonable amount of time, this method should just return. + # + # If when this method returns, the machine's state isn't "powered_off," + # Vagrant will proceed to forcefully shut the machine down. + def halt; end + # Mounts a shared folder. This method is called by the shared # folder action with an open SSH session (passed in as `ssh`). # This method should create, mount, and properly set permissions diff --git a/lib/vagrant/systems/linux.rb b/lib/vagrant/systems/linux.rb index 778275591..9eea6d6ea 100644 --- a/lib/vagrant/systems/linux.rb +++ b/lib/vagrant/systems/linux.rb @@ -11,6 +11,24 @@ module Vagrant #------------------------------------------------------------------- # Overridden methods #------------------------------------------------------------------- + def halt + logger.info "Attempting graceful shutdown of linux..." + vm.env.ssh.execute do |ssh| + ssh.exec!("sudo halt") + end + + # 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.vm.state(true) != :powered_off + count += 1 + + return if count >= 15 + sleep 1 + end + end + def mount_shared_folder(ssh, name, guestpath) ssh.exec!("sudo mkdir -p #{guestpath}") mount_folder(ssh, name, guestpath) diff --git a/test/test_helper.rb b/test/test_helper.rb index 65de972e6..6f5eebd3c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -87,9 +87,16 @@ class Test::Unit::TestCase mock_vm.stubs(:actions).returns([action]) mock_vm.stubs(:env).returns(mock_environment) + vm.stubs(:env).returns(mock_vm.env) + [mock_vm, vm, action] end + # Returns a linux system + def linux_system(vm) + Vagrant::Systems::Linux.new(vm) + end + def stub_default_action_dependecies(mock) mock.stubs(:precedes).returns([]) mock.stubs(:follows).returns([]) diff --git a/test/vagrant/actions/vm/halt_test.rb b/test/vagrant/actions/vm/halt_test.rb index 49470ac44..a96003713 100644 --- a/test/vagrant/actions/vm/halt_test.rb +++ b/test/vagrant/actions/vm/halt_test.rb @@ -3,21 +3,35 @@ require File.join(File.dirname(__FILE__), '..', '..', '..', 'test_helper') class HaltActionTest < Test::Unit::TestCase setup do @runner, @vm, @action = mock_action(Vagrant::Actions::VM::Halt) + @runner.stubs(:system).returns(linux_system(@vm)) end context "executing" do setup do @vm.stubs(:running?).returns(true) + + @runner.system.stubs(:halt) + @vm.stubs(:stop) + @vm.stubs(:state).returns(:powered_off) end should "invoke the 'halt' around callback" do - halt_seq = sequence("halt_seq") - @runner.expects(:invoke_around_callback).with(:halt).once.in_sequence(halt_seq).yields - @vm.expects(:stop).in_sequence(halt_seq) + @runner.expects(:invoke_around_callback).with(:halt).once @action.execute! end - should "force the VM to stop" do + should "halt with the system and NOT force VM to stop if powered off" do + @vm.expects(:state).with(true).returns(:powered_off) + + @runner.system.expects(:halt).once + @vm.expects(:stop).never + @action.execute! + end + + should "halt with the system and force VM to stop if NOT powered off" do + @vm.expects(:state).with(true).returns(:running) + + @runner.system.expects(:halt).once @vm.expects(:stop).once @action.execute! end diff --git a/test/vagrant/actions/vm/shared_folders_test.rb b/test/vagrant/actions/vm/shared_folders_test.rb index 56804cc70..d5fc41676 100644 --- a/test/vagrant/actions/vm/shared_folders_test.rb +++ b/test/vagrant/actions/vm/shared_folders_test.rb @@ -3,7 +3,7 @@ require File.join(File.dirname(__FILE__), '..', '..', '..', 'test_helper') class SharedFoldersActionTest < Test::Unit::TestCase setup do @runner, @vm, @action = mock_action(Vagrant::Actions::VM::SharedFolders) - @runner.stubs(:system).returns(Vagrant::Systems::Linux.new(@vm)) + @runner.stubs(:system).returns(linux_system(@vm)) end def stub_shared_folders diff --git a/test/vagrant/systems/linux_test.rb b/test/vagrant/systems/linux_test.rb index 017b74826..fb473aff8 100644 --- a/test/vagrant/systems/linux_test.rb +++ b/test/vagrant/systems/linux_test.rb @@ -9,6 +9,26 @@ class LinuxSystemTest < Test::Unit::TestCase @instance = @klass.new(@vm) end + context "halting" do + setup do + @ssh_session = mock("ssh_session") + @ssh = mock("ssh") + @ssh.stubs(:execute).yields(@ssh_session) + @vm.env.stubs(:ssh).returns(@ssh) + + @real_vm = mock("real_vm") + @real_vm.stubs(:state).returns(:powered_off) + @vm.stubs(:vm).returns(@real_vm) + end + + should "execute halt via SSH" do + @ssh_session.expects(:exec!).with("sudo halt").once + @instance.halt + end + + # TODO: Sleep/timeout testing + end + context "mounting shared folders" do setup do @ssh = mock("ssh")