diff options
-rw-r--r-- | lib/chef/client.rb | 9 | ||||
-rw-r--r-- | lib/chef/dsl/reboot_pending.rb | 9 | ||||
-rw-r--r-- | lib/chef/platform/rebooter.rb | 54 | ||||
-rw-r--r-- | lib/chef/provider/reboot.rb | 69 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/reboot.rb | 48 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | lib/chef/run_context.rb | 25 | ||||
-rw-r--r-- | spec/functional/rebooter_spec.rb | 105 | ||||
-rw-r--r-- | spec/functional/resource/reboot_spec.rb | 103 | ||||
-rw-r--r-- | spec/unit/dsl/reboot_pending_spec.rb | 8 | ||||
-rw-r--r-- | spec/unit/run_context_spec.rb | 15 |
12 files changed, 436 insertions, 11 deletions
diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 2de3ca3e64..161ecddb0f 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -44,6 +44,7 @@ require 'chef/resource_reporter' require 'chef/run_lock' require 'chef/policy_builder' require 'chef/request_id' +require 'chef/platform/rebooter' require 'ohai' require 'rbconfig' @@ -427,7 +428,9 @@ class Chef run_context = setup_run_context - converge(run_context) + catch (:end_client_run_early) do + converge(run_context) + end save_updated_node @@ -435,6 +438,10 @@ class Chef Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") run_completed_successfully @events.run_completed(node) + + # rebooting has to be the last thing we do, no exceptions. + Chef::Platform::Rebooter.reboot_if_needed!(node) + true rescue Exception => e # CHEF-3336: Send the error first in case something goes wrong below and we don't know why diff --git a/lib/chef/dsl/reboot_pending.rb b/lib/chef/dsl/reboot_pending.rb index 9f80d38c61..a81debce99 100644 --- a/lib/chef/dsl/reboot_pending.rb +++ b/lib/chef/dsl/reboot_pending.rb @@ -27,10 +27,13 @@ class Chef include Chef::DSL::PlatformIntrospection # Returns true if the system needs a reboot or is expected to reboot - # Raises UnsupportedPlatform if this functionality isn't provided yet + # Note that we will silently miss any other platform-specific reboot notices besides Windows+Ubuntu. def reboot_pending? - if platform?("windows") + # don't break when used as a mixin in contexts without #node (e.g. specs). + if self.respond_to?(:node, true) && node.run_context.reboot_requested? + true + elsif platform?("windows") # PendingFileRenameOperations contains pairs (REG_MULTI_SZ) of filenames that cannot be updated # due to a file being in use (usually a temporary file and a system file) # \??\c:\temp\test.sys!\??\c:\winnt\system32\test.sys @@ -53,7 +56,7 @@ class Chef # This should work for Debian as well if update-notifier-common happens to be installed. We need an API for that. File.exists?('/var/run/reboot-required') else - raise Chef::Exceptions::UnsupportedPlatform.new(node[:platform]) + false end end end diff --git a/lib/chef/platform/rebooter.rb b/lib/chef/platform/rebooter.rb new file mode 100644 index 0000000000..b46f0e394c --- /dev/null +++ b/lib/chef/platform/rebooter.rb @@ -0,0 +1,54 @@ +# +# Author:: Chris Doherty <cdoherty@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/dsl/reboot_pending' +require 'chef/log' +require 'chef/platform' + +class Chef + class Platform + module Rebooter + extend Chef::Mixin::ShellOut + + class << self + + def reboot!(node) + reboot_info = node.run_context.reboot_info + + cmd = if Chef::Platform.windows? + # should this do /f as well? do we then need a minimum delay to let apps quit? + "shutdown /r /t #{reboot_info[:delay_mins]} /c \"#{reboot_info[:reason]}\"" + else + # probably Linux-only. + "shutdown -r +#{reboot_info[:delay_mins]} \"#{reboot_info[:reason]}\"" + end + + Chef::Log.warn "Rebooting server at a recipe's request. Details: #{reboot_info.inspect}" + shell_out!(cmd) + end + + # this is a wrapper function so Chef::Client only needs a single line of code. + def reboot_if_needed!(node) + if node.run_context.reboot_requested? + reboot!(node) + end + end + end + end + end +end diff --git a/lib/chef/provider/reboot.rb b/lib/chef/provider/reboot.rb new file mode 100644 index 0000000000..8dde4653ec --- /dev/null +++ b/lib/chef/provider/reboot.rb @@ -0,0 +1,69 @@ +# +# Author:: Chris Doherty <cdoherty@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/log' +require 'chef/provider' + +class Chef + class Provider + class Reboot < Chef::Provider + + def whyrun_supported? + true + end + + def load_current_resource + @current_resource ||= Chef::Resource::Reboot.new(@new_resource.name) + @current_resource.reason(@new_resource.reason) + @current_resource.delay_mins(@new_resource.delay_mins) + @current_resource + end + + def request_reboot + node.run_context.request_reboot( + :delay_mins => @new_resource.delay_mins, + :reason => @new_resource.reason, + :timestamp => Time.now, + :requested_by => @new_resource.name + ) + end + + def action_request_reboot + converge_by("request a system reboot to occur if the run succeeds") do + Chef::Log.warn "Reboot requested:'#{@new_resource.name}'" + request_reboot + end + end + + def action_reboot_now + converge_by("rebooting the system immediately") do + Chef::Log.warn "Rebooting system immediately, requested by '#{@new_resource.name}'" + request_reboot + throw :end_client_run_early + end + end + + def action_cancel + converge_by("cancel any existing end-of-run reboot request") do + Chef::Log.warn "Reboot canceled: '#{@new_resource.name}'" + node.run_context.cancel_reboot + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 3c9e94e6f7..1b0ff3ffff 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -39,6 +39,7 @@ require 'chef/provider/mdadm' require 'chef/provider/mount' require 'chef/provider/package' require 'chef/provider/powershell_script' +require 'chef/provider/reboot' require 'chef/provider/remote_directory' require 'chef/provider/remote_file' require 'chef/provider/route' diff --git a/lib/chef/resource/reboot.rb b/lib/chef/resource/reboot.rb new file mode 100644 index 0000000000..d6caafdea8 --- /dev/null +++ b/lib/chef/resource/reboot.rb @@ -0,0 +1,48 @@ +# +# Author:: Chris Doherty <cdoherty@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/resource' + +# In using this resource via notifications, it's important to *only* use +# immediate notifications. Delayed notifications produce unintuitive and +# probably undesired results. +class Chef + class Resource + class Reboot < Chef::Resource + def initialize(name, run_context=nil) + super + @resource_name = :reboot + @provider = Chef::Provider::Reboot + @allowed_actions = [:request_reboot, :reboot_now, :cancel] + + @reason = "Reboot by Chef" + @delay_mins = 0 + + # no default action. + end + + def reason(arg=nil) + set_or_return(:reason, arg, :kind_of => String) + end + + def delay_mins(arg=nil) + set_or_return(:delay_mins, arg, :kind_of => Fixnum) + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 93ff682288..8c2f71bd30 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -53,6 +53,7 @@ require 'chef/resource/perl' require 'chef/resource/portage_package' require 'chef/resource/powershell_script' require 'chef/resource/python' +require 'chef/resource/reboot' require 'chef/resource/registry_key' require 'chef/resource/remote_directory' require 'chef/resource/remote_file' diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 3dd53f0f8f..bbe2f9eba0 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -61,6 +61,9 @@ class Chef # Event dispatcher for this run. attr_reader :events + # Hash of factoids for a reboot request. + attr_reader :reboot_info + # Creates a new Chef::RunContext object and populates its fields. This object gets # used by the Chef Server to generate a fully compiled recipe list for a node. # @@ -76,6 +79,7 @@ class Chef @loaded_recipes = {} @loaded_attributes = {} @events = events + @reboot_info = {} @node.run_context = self @@ -271,6 +275,27 @@ ERROR_MESSAGE end end + # there are options for how to handle multiple calls to these functions: + # 1. first call always wins (never change @reboot_info once set). + # 2. last call always wins (happily change @reboot_info whenever). + # 3. raise an exception on the first conflict. + # 4. disable reboot after this run if anyone ever calls :cancel. + # 5. raise an exception on any second call. + # 6. ? + def request_reboot(reboot_info) + Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to #{reboot_info.inspect}" + @reboot_info = reboot_info + end + + def cancel_reboot + Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to {}" + @reboot_info = {} + end + + def reboot_requested? + @reboot_info.size > 0 + end + private def loaded_recipe(cookbook, recipe) diff --git a/spec/functional/rebooter_spec.rb b/spec/functional/rebooter_spec.rb new file mode 100644 index 0000000000..8006580d5c --- /dev/null +++ b/spec/functional/rebooter_spec.rb @@ -0,0 +1,105 @@ +# +# Author:: Chris Doherty <cdoherty@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Platform::Rebooter do + + let(:reboot_info) do + { + :delay_mins => 5, + :requested_by => "reboot resource functional test", + :reason => "rebooter spec test" + } + end + + def create_resource + resource = Chef::Resource::Reboot.new(expected[:requested_by], run_context) + resource.delay_mins(expected[:delay_mins]) + resource.reason(expected[:reason]) + resource + end + + let(:run_context) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, events) + end + + let(:expected) do + { + :windows => 'shutdown /r /t 5 /c "rebooter spec test"', + :linux => 'shutdown -r +5 "rebooter spec test"' + } + end + + let(:rebooter) { Chef::Platform::Rebooter } + + describe '#reboot_if_needed!' do + + it 'should not call #shell_out! when reboot has not been requested' do + expect(rebooter).to receive(:shell_out!).exactly(0).times + expect(rebooter).to receive(:reboot_if_needed!).once.and_call_original + rebooter.reboot_if_needed!(run_context.node) + end + + describe 'calling #shell_out! to reboot' do + + before(:each) do + run_context.request_reboot(reboot_info) + end + + after(:each) do + run_context.cancel_reboot + end + + shared_context 'test a reboot method' do + def test_rebooter_method(method_sym, is_windows, expected_reboot_str) + Chef::Platform.stub(:windows?).and_return(is_windows) + expect(rebooter).to receive(:shell_out!).once.with(expected_reboot_str) + expect(rebooter).to receive(method_sym).once.and_call_original + rebooter.send(method_sym, run_context.node) + end + end + + describe 'when using #reboot_if_needed!' do + include_context 'test a reboot method' + + it 'should produce the correct string on Windows' do + test_rebooter_method(:reboot_if_needed!, true, expected[:windows]) + end + + it 'should produce the correct (Linux-specific) string on non-Windows' do + test_rebooter_method(:reboot_if_needed!, false, expected[:linux]) + end + end + + describe 'when using #reboot!' do + include_context 'test a reboot method' + + it 'should produce the correct string on Windows' do + test_rebooter_method(:reboot!, true, expected[:windows]) + end + + it 'should produce the correct (Linux-specific) string on non-Windows' do + test_rebooter_method(:reboot!, false, expected[:linux]) + end + end + end + end +end diff --git a/spec/functional/resource/reboot_spec.rb b/spec/functional/resource/reboot_spec.rb new file mode 100644 index 0000000000..735ca994c8 --- /dev/null +++ b/spec/functional/resource/reboot_spec.rb @@ -0,0 +1,103 @@ +# +# Author:: Chris Doherty <cdoherty@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Resource::Reboot do + + let(:expected) do + { + :delay_mins => 5, + :requested_by => "reboot resource functional test", + :reason => "reboot resource spec test" + } + end + + def create_resource + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + resource = Chef::Resource::Reboot.new(expected[:requested_by], run_context) + resource.delay_mins(expected[:delay_mins]) + resource.reason(expected[:reason]) + resource + end + + let(:resource) do + create_resource + end + + shared_context 'testing run context modification' do + def test_reboot_action(resource) + reboot_info = resource.run_context.reboot_info + expect(reboot_info.keys.sort).to eq([:delay_mins, :reason, :requested_by, :timestamp]) + expect(reboot_info[:delay_mins]).to eq(expected[:delay_mins]) + expect(reboot_info[:reason]).to eq(expected[:reason]) + expect(reboot_info[:requested_by]).to eq(expected[:requested_by]) + + expect(resource.run_context.reboot_requested?).to be_true + end + end + + # the currently defined behavior for multiple calls to this resource is "last one wins." + describe 'the request_reboot_on_successful_run action' do + include_context 'testing run context modification' + + before do + resource.run_action(:request_reboot) + end + + after do + resource.run_context.cancel_reboot + end + + it 'should have modified the run context correctly' do + test_reboot_action(resource) + end + end + + describe 'the reboot_interrupt_run action' do + include_context 'testing run context modification' + + after do + resource.run_context.cancel_reboot + end + + it 'should have modified the run context correctly' do + # this doesn't actually test the flow of Chef::Client#do_run, unfortunately. + expect { + resource.run_action(:reboot_now) + }.to throw_symbol(:end_client_run_early) + + test_reboot_action(resource) + end + end + + describe "the cancel action" do + before do + resource.run_context.request_reboot(expected) + resource.run_action(:cancel) + end + + it 'should have cleared the reboot request' do + # arguably we shouldn't be querying RunContext's internal data directly. + expect(resource.run_context.reboot_info).to eq({}) + expect(resource.run_context.reboot_requested?).to be_false + end + end +end diff --git a/spec/unit/dsl/reboot_pending_spec.rb b/spec/unit/dsl/reboot_pending_spec.rb index 8576ae168a..0d643514e0 100644 --- a/spec/unit/dsl/reboot_pending_spec.rb +++ b/spec/unit/dsl/reboot_pending_spec.rb @@ -21,7 +21,7 @@ require "spec_helper" describe Chef::DSL::RebootPending do describe "reboot_pending?" do - describe "in isoloation" do + describe "in isolation" do let(:recipe) { Object.new.extend(Chef::DSL::RebootPending) } before do @@ -74,12 +74,6 @@ describe Chef::DSL::RebootPending do end end - context "platform is not supported" do - it 'should raise an exception' do - recipe.stub_chain(:node, :[]).with(:platform).and_return('msdos') - expect { recipe.reboot_pending? }.to raise_error(Chef::Exceptions::UnsupportedPlatform) - end - end end # describe in isolation describe "in a recipe" do diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index 1def10faf5..21ece2abaa 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -134,4 +134,19 @@ describe Chef::RunContext do end end + describe "handling reboot requests" do + let(:expected) do + { :reason => "spec tests require a reboot" } + end + + it "stores and deletes the reboot request" do + @run_context.request_reboot(expected) + expect(@run_context.reboot_info).to eq(expected) + expect(@run_context.reboot_requested?).to be_true + + @run_context.cancel_reboot + expect(@run_context.reboot_info).to eq({}) + expect(@run_context.reboot_requested?).to be_false + end + end end |