summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Doherty <randomcamel@users.noreply.github.com>2014-09-12 17:24:16 -0700
committerChris Doherty <randomcamel@users.noreply.github.com>2014-09-12 17:24:16 -0700
commitcb61daebfb0d255cae928ca1a92db29b055755cf (patch)
treeac2936ed19cfe5f608189aec18ccebe6ee4ad647
parent1241ea6f9866d0e61d11129bb32e5fc96cd2bac0 (diff)
parentb6cc74645ead494c35d1ea4ecac1ce039fcd02b4 (diff)
downloadchef-cb61daebfb0d255cae928ca1a92db29b055755cf.tar.gz
Merge pull request #1979 from opscode/cdoherty-reboot-core
Add a Reboot resource into core.
-rw-r--r--lib/chef/client.rb9
-rw-r--r--lib/chef/dsl/reboot_pending.rb9
-rw-r--r--lib/chef/platform/rebooter.rb54
-rw-r--r--lib/chef/provider/reboot.rb69
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/reboot.rb48
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/run_context.rb25
-rw-r--r--spec/functional/rebooter_spec.rb105
-rw-r--r--spec/functional/resource/reboot_spec.rb103
-rw-r--r--spec/unit/dsl/reboot_pending_spec.rb8
-rw-r--r--spec/unit/run_context_spec.rb15
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