summaryrefslogtreecommitdiff
path: root/lib/chef
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef')
-rw-r--r--lib/chef/application.rb23
-rw-r--r--lib/chef/application/apply.rb8
-rw-r--r--lib/chef/application/client.rb6
-rw-r--r--lib/chef/application/exit_code.rb226
-rw-r--r--lib/chef/application/solo.rb4
-rw-r--r--lib/chef/application/windows_service.rb6
-rw-r--r--lib/chef/config_fetcher.rb8
-rw-r--r--lib/chef/exceptions.rb23
-rw-r--r--lib/chef/platform/rebooter.rb15
9 files changed, 288 insertions, 31 deletions
diff --git a/lib/chef/application.rb b/lib/chef/application.rb
index 7dbffd8dec..f8df71f723 100644
--- a/lib/chef/application.rb
+++ b/lib/chef/application.rb
@@ -27,6 +27,7 @@ require "chef/platform"
require "mixlib/cli"
require "tmpdir"
require "rbconfig"
+require "chef/application/exit_code"
class Chef
class Application
@@ -60,11 +61,11 @@ class Chef
def setup_signal_handlers
trap("INT") do
- Chef::Application.fatal!("SIGINT received, stopping", 2)
+ Chef::Application.fatal!("SIGINT received, stopping", Chef::Exceptions::SigInt.new)
end
trap("TERM") do
- Chef::Application.fatal!("SIGTERM received, stopping", 3)
+ Chef::Application.fatal!("SIGTERM received, stopping", Chef::Exceptions::SigTerm.new)
end
unless Chef::Platform.windows?
@@ -149,7 +150,7 @@ class Chef
Chef::Log.level = resolve_log_level
rescue StandardError => error
Chef::Log.fatal("Failed to open or create log file at #{Chef::Config[:log_location]}: #{error.class} (#{error.message})")
- Chef::Application.fatal!("Aborting due to invalid 'log_location' configuration", 2)
+ Chef::Application.fatal!("Aborting due to invalid 'log_location' configuration", error)
end
# Turn `log_location :syslog` and `log_location :win_evt` into the
@@ -285,7 +286,7 @@ class Chef
@chef_client.run
rescue Exception => e
Chef::Log.error(e.to_s)
- exit 1
+ exit Chef::Application.normalize_exit_code(e)
else
exit 0
end
@@ -314,7 +315,7 @@ class Chef
Chef::Log.fatal("Configuration error #{error.class}: #{error.message}")
filtered_trace = error.backtrace.grep(/#{Regexp.escape(config_file_path)}/)
filtered_trace.each { |line| Chef::Log.fatal(" " + line ) }
- Chef::Application.fatal!("Aborting due to error in '#{config_file_path}'", 2)
+ Chef::Application.fatal!("Aborting due to error in '#{config_file_path}'", error)
end
# This is a hook for testing
@@ -341,15 +342,19 @@ class Chef
true
end
+ def normalize_exit_code(exit_code)
+ Chef::Application::ExitCode.normalize_exit_code(exit_code)
+ end
+
# Log a fatal error message to both STDERR and the Logger, exit the application
- def fatal!(msg, err = -1)
+ def fatal!(msg, err = nil)
Chef::Log.fatal(msg)
- Process.exit err
+ Process.exit(normalize_exit_code(err))
end
- def exit!(msg, err = -1)
+ def exit!(msg, err = nil)
Chef::Log.debug(msg)
- Process.exit err
+ Process.exit(normalize_exit_code(err))
end
end
diff --git a/lib/chef/application/apply.rb b/lib/chef/application/apply.rb
index 37ddcb3164..3e3fb58448 100644
--- a/lib/chef/application/apply.rb
+++ b/lib/chef/application/apply.rb
@@ -137,11 +137,11 @@ class Chef::Application::Apply < Chef::Application
def read_recipe_file(file_name)
if file_name.nil?
- Chef::Application.fatal!("No recipe file was provided", 1)
+ Chef::Application.fatal!("No recipe file was provided", Chef::Exceptions::RecipeNotFound.new)
else
recipe_path = File.expand_path(file_name)
unless File.exist?(recipe_path)
- Chef::Application.fatal!("No file exists at #{recipe_path}", 1)
+ Chef::Application.fatal!("No file exists at #{recipe_path}", Chef::Exceptions::RecipeNotFound.new)
end
recipe_fh = open(recipe_path)
recipe_text = recipe_fh.read
@@ -183,7 +183,7 @@ class Chef::Application::Apply < Chef::Application
else
if !ARGV[0]
puts opt_parser
- Chef::Application.exit! "No recipe file provided", 1
+ Chef::Application.exit! "No recipe file provided", Chef::Exceptions::RecipeNotFound.new
end
@recipe_filename = ARGV[0]
@recipe_text, @recipe_fh = read_recipe_file @recipe_filename
@@ -208,7 +208,7 @@ class Chef::Application::Apply < Chef::Application
raise
rescue Exception => e
Chef::Application.debug_stacktrace(e)
- Chef::Application.fatal!("#{e.class}: #{e.message}", 1)
+ Chef::Application.fatal!("#{e.class}: #{e.message}", e)
end
end
diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb
index ac46e533dd..77c86ad559 100644
--- a/lib/chef/application/client.rb
+++ b/lib/chef/application/client.rb
@@ -324,7 +324,7 @@ class Chef::Application::Client < Chef::Application
if Chef::Config[:recipe_url]
if !Chef::Config.local_mode
- Chef::Application.fatal!("chef-client recipe-url can be used only in local-mode", 1)
+ Chef::Application.fatal!("chef-client recipe-url can be used only in local-mode")
else
if Chef::Config[:delete_entire_chef_repo]
Chef::Log.debug "Cleanup path #{Chef::Config.chef_repo_path} before extract recipes into it"
@@ -420,7 +420,7 @@ class Chef::Application::Client < Chef::Application
rescue SystemExit
raise
rescue Exception => e
- Chef::Application.fatal!("#{e.class}: #{e.message}", 1)
+ Chef::Application.fatal!("#{e.class}: #{e.message}", e)
end
else
interval_run_chef_client
@@ -463,7 +463,7 @@ class Chef::Application::Client < Chef::Application
retry
end
- Chef::Application.fatal!("#{e.class}: #{e.message}", 1)
+ Chef::Application.fatal!("#{e.class}: #{e.message}", e)
end
def test_signal
diff --git a/lib/chef/application/exit_code.rb b/lib/chef/application/exit_code.rb
new file mode 100644
index 0000000000..753f1a0d80
--- /dev/null
+++ b/lib/chef/application/exit_code.rb
@@ -0,0 +1,226 @@
+#
+# Author:: Steven Murawski (<smurawski@chef.io>)
+# Copyright:: Copyright 2016, Chef Software, 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.
+#
+
+class Chef
+ class Application
+
+ # These are the exit codes defined in Chef RFC 062
+ # https://github.com/chef/chef-rfc/blob/master/rfc062-exit-status.md
+ class ExitCode
+
+ # -1 is defined as DEPRECATED_FAILURE in RFC 062, so it is
+ # not enumerated in an active constant.
+ #
+ VALID_RFC_062_EXIT_CODES = {
+ SUCCESS: 0,
+ GENERIC_FAILURE: 1,
+ SIGINT_RECEIVED: 2,
+ SIGTERM_RECEIVED: 3,
+ REBOOT_SCHEDULED: 35,
+ REBOOT_NEEDED: 37,
+ REBOOT_FAILED: 41,
+ AUDIT_MODE_FAILURE: 42,
+ }
+
+ DEPRECATED_RFC_062_EXIT_CODES = {
+ DEPRECATED_FAILURE: -1,
+ }
+
+ class << self
+
+ def normalize_exit_code(exit_code = nil)
+ if normalization_not_configured?
+ normalize_legacy_exit_code_with_warning(exit_code)
+ elsif normalization_disabled?
+ normalize_legacy_exit_code(exit_code)
+ else
+ normalize_exit_code_to_rfc(exit_code)
+ end
+ end
+
+ def enforce_rfc_062_exit_codes?
+ !normalization_disabled? && !normalization_not_configured?
+ end
+
+ def notify_reboot_exit_code_deprecation
+ return if normalization_disabled?
+ notify_on_deprecation(reboot_deprecation_warning)
+ end
+
+ def notify_deprecated_exit_code
+ return if normalization_disabled?
+ notify_on_deprecation(deprecation_warning)
+ end
+
+ private
+
+ def normalization_disabled?
+ Chef::Config[:exit_status] == :disabled
+ end
+
+ def normalization_not_configured?
+ Chef::Config[:exit_status].nil?
+ end
+
+ def normalize_legacy_exit_code_with_warning(exit_code)
+ normalized_exit_code = normalize_legacy_exit_code(exit_code)
+ unless valid_exit_codes.include? normalized_exit_code
+ notify_on_deprecation(deprecation_warning)
+ end
+ normalized_exit_code
+ end
+
+ def normalize_legacy_exit_code(exit_code)
+ case exit_code
+ when Fixnum
+ exit_code
+ when Exception
+ lookup_exit_code_by_exception(exit_code)
+ else
+ default_exit_code
+ end
+ end
+
+ def normalize_exit_code_to_rfc(exit_code)
+ normalized_exit_code = normalize_legacy_exit_code_with_warning(exit_code)
+ if valid_exit_codes.include? normalized_exit_code
+ normalized_exit_code
+ else
+ VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
+ end
+ end
+
+ def lookup_exit_code_by_exception(exception)
+ if sigint_received?(exception)
+ VALID_RFC_062_EXIT_CODES[:SIGINT_RECEIVED]
+ elsif sigterm_received?(exception)
+ VALID_RFC_062_EXIT_CODES[:SIGTERM_RECEIVED]
+ elsif normalization_disabled? || normalization_not_configured?
+ if legacy_exit_code?(exception)
+ # We have lots of "Chef::Application.fatal!('', 2)
+ # This maintains that behavior at initial introduction
+ # and when the RFC exit_status compliance is disabled.
+ VALID_RFC_062_EXIT_CODES[:SIGINT_RECEIVED]
+ else
+ VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
+ end
+ elsif reboot_scheduled?(exception)
+ VALID_RFC_062_EXIT_CODES[:REBOOT_SCHEDULED]
+ elsif reboot_needed?(exception)
+ VALID_RFC_062_EXIT_CODES[:REBOOT_NEEDED]
+ elsif reboot_failed?(exception)
+ VALID_RFC_062_EXIT_CODES[:REBOOT_FAILED]
+ elsif audit_failure?(exception)
+ VALID_RFC_062_EXIT_CODES[:AUDIT_MODE_FAILURE]
+ else
+ VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
+ end
+ end
+
+ def legacy_exit_code?(exception)
+ resolve_exception_array(exception).any? do |e|
+ e.is_a? Chef::Exceptions::DeprecatedExitCode
+ end
+ end
+
+ def reboot_scheduled?(exception)
+ resolve_exception_array(exception).any? do |e|
+ e.is_a? Chef::Exceptions::Reboot
+ end
+ end
+
+ def reboot_needed?(exception)
+ resolve_exception_array(exception).any? do |e|
+ e.is_a? Chef::Exceptions::RebootPending
+ end
+ end
+
+ def reboot_failed?(exception)
+ resolve_exception_array(exception).any? do |e|
+ e.is_a? Chef::Exceptions::RebootFailed
+ end
+ end
+
+ def audit_failure?(exception)
+ resolve_exception_array(exception).any? do |e|
+ e.is_a? Chef::Exceptions::AuditError
+ end
+ end
+
+ def sigint_received?(exception)
+ resolve_exception_array(exception).any? do |e|
+ e.is_a? Chef::Exceptions::SigInt
+ end
+ end
+
+ def sigterm_received?(exception)
+ resolve_exception_array(exception).any? do |e|
+ e.is_a? Chef::Exceptions::SigTerm
+ end
+ end
+
+ def resolve_exception_array(exception)
+ exception_array = [exception]
+ if exception.respond_to?(:wrapped_errors)
+ exception.wrapped_errors.each do |e|
+ exception_array.push e
+ end
+ end
+ exception_array
+ end
+
+ def valid_exit_codes
+ VALID_RFC_062_EXIT_CODES.values
+ end
+
+ def notify_on_deprecation(message)
+ begin
+ Chef.log_deprecation(message)
+ rescue Chef::Exceptions::DeprecatedFeatureError
+ # Have to rescue this, otherwise this unhandled error preempts
+ # the current exit code assignment.
+ end
+ end
+
+ def deprecation_warning
+ "Chef RFC 062 (https://github.com/chef/chef-rfc/master/rfc062-exit-status.md) defines the" \
+ " exit codes that should be used with Chef. Chef::Application::ExitCode defines valid exit codes" \
+ " In a future release, non-standard exit codes will be redefined as" \
+ " GENERIC_FAILURE unless `exit_status` is set to `:disabled` in your client.rb."
+ end
+
+ def reboot_deprecation_warning
+ "Per RFC 062 (https://github.com/chef/chef-rfc/blob/master/rfc062-exit-status.md)" \
+ ", when a reboot is requested Chef Client will exit with an exit code of 35, REBOOT_SCHEDULED." \
+ " To maintain the current behavior (an exit code of 0), you will need to set `exit_status` to" \
+ " `:disabled` in your client.rb"
+ end
+
+ def default_exit_code
+ if normalization_disabled? || normalization_not_configured?
+ return DEPRECATED_RFC_062_EXIT_CODES[:DEPRECATED_FAILURE]
+ else
+ VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
+ end
+ end
+
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb
index bc2d279508..0c66f6cf40 100644
--- a/lib/chef/application/solo.rb
+++ b/lib/chef/application/solo.rb
@@ -285,7 +285,7 @@ class Chef::Application::Solo < Chef::Application
rescue SystemExit
raise
rescue Exception => e
- Chef::Application.fatal!("#{e.class}: #{e.message}", 1)
+ Chef::Application.fatal!("#{e.class}: #{e.message}", e)
end
else
interval_run_chef_client
@@ -332,7 +332,7 @@ EOH
Chef::Log.debug("#{e.class}: #{e}\n#{e.backtrace.join("\n")}")
retry
else
- Chef::Application.fatal!("#{e.class}: #{e.message}", 1)
+ Chef::Application.fatal!("#{e.class}: #{e.message}", e)
end
end
end
diff --git a/lib/chef/application/windows_service.rb b/lib/chef/application/windows_service.rb
index fca1ed3689..2f1456ac45 100644
--- a/lib/chef/application/windows_service.rb
+++ b/lib/chef/application/windows_service.rb
@@ -319,11 +319,11 @@ class Chef
Chef::Config.merge!(config)
rescue SocketError
- Chef::Application.fatal!("Error getting config file #{Chef::Config[:config_file]}", 2)
+ Chef::Application.fatal!("Error getting config file #{Chef::Config[:config_file]}", Chef::Exceptions::DeprecatedExitCode.new)
rescue Chef::Exceptions::ConfigurationError => error
- Chef::Application.fatal!("Error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2)
+ Chef::Application.fatal!("Error processing config file #{Chef::Config[:config_file]} with error #{error.message}", Chef::Exceptions::DeprecatedExitCode.new)
rescue Exception => error
- Chef::Application.fatal!("Unknown error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2)
+ Chef::Application.fatal!("Unknown error processing config file #{Chef::Config[:config_file]} with error #{error.message}", Chef::Exceptions::DeprecatedExitCode.new)
end
end
diff --git a/lib/chef/config_fetcher.rb b/lib/chef/config_fetcher.rb
index acd2f07f5e..ee1b64956a 100644
--- a/lib/chef/config_fetcher.rb
+++ b/lib/chef/config_fetcher.rb
@@ -25,7 +25,7 @@ class Chef
begin
Chef::JSONCompat.from_json(config_data)
rescue Chef::Exceptions::JSON::ParseError => error
- Chef::Application.fatal!("Could not parse the provided JSON file (#{config_location}): " + error.message, 2)
+ Chef::Application.fatal!("Could not parse the provided JSON file (#{config_location}): " + error.message, Chef::Exceptions::DeprecatedExitCode.new)
end
end
@@ -40,15 +40,15 @@ class Chef
def fetch_remote_config
http.get("")
rescue SocketError, SystemCallError, Net::HTTPServerException => error
- Chef::Application.fatal!("Cannot fetch config '#{config_location}': '#{error.class}: #{error.message}", 2)
+ Chef::Application.fatal!("Cannot fetch config '#{config_location}': '#{error.class}: #{error.message}", Chef::Exceptions::DeprecatedExitCode.new)
end
def read_local_config
::File.read(config_location)
rescue Errno::ENOENT
- Chef::Application.fatal!("Cannot load configuration from #{config_location}", 2)
+ Chef::Application.fatal!("Cannot load configuration from #{config_location}", Chef::Exceptions::DeprecatedExitCode.new)
rescue Errno::EACCES
- Chef::Application.fatal!("Permissions are incorrect on #{config_location}. Please chmod a+r #{config_location}", 2)
+ Chef::Application.fatal!("Permissions are incorrect on #{config_location}. Please chmod a+r #{config_location}", Chef::Exceptions::DeprecatedExitCode.new)
end
def config_missing?
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 6afcc9c51e..ea90d80cd8 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -42,6 +42,8 @@ class Chef
end
class Application < RuntimeError; end
+ class SigInt < RuntimeError; end
+ class SigTerm < RuntimeError; end
class Cron < RuntimeError; end
class Env < RuntimeError; end
class Exec < RuntimeError; end
@@ -56,6 +58,14 @@ class Chef
class UnsupportedAction < RuntimeError; end
class MissingLibrary < RuntimeError; end
+ class DeprecatedExitCode < RuntimeError
+ def initalize
+ super "Exiting with a non RFC 062 Exit Code."
+ require "chef/application/exit_code"
+ Chef::Application::ExitCode.notify_deprecated_exit_code
+ end
+ end
+
class CannotDetermineNodeName < RuntimeError
def initialize
super "Unable to determine node name: configure node_name or configure the system's hostname and fqdn"
@@ -66,6 +76,9 @@ class Chef
class Group < RuntimeError; end
class Link < RuntimeError; end
class Mount < RuntimeError; end
+ class Reboot < Exception; end
+ class RebootPending < Exception; end
+ class RebootFailed < Mixlib::ShellOut::ShellCommandFailed; end
class PrivateKeyMissing < RuntimeError; end
class CannotWritePrivateKey < RuntimeError; end
class RoleNotFound < RuntimeError; end
@@ -426,18 +439,20 @@ This error is most often caused by network issues (proxies, etc) outside of chef
end
end
- class AuditControlGroupDuplicate < RuntimeError
+ class AuditError < RuntimeError; end
+
+ class AuditControlGroupDuplicate < AuditError
def initialize(name)
super "Control group with name '#{name}' has already been defined"
end
end
- class AuditNameMissing < RuntimeError; end
- class NoAuditsProvided < RuntimeError
+ class AuditNameMissing < AuditError; end
+ class NoAuditsProvided < AuditError
def initialize
super "You must provide a block with controls"
end
end
- class AuditsFailed < RuntimeError
+ class AuditsFailed < AuditError
def initialize(num_failed, num_total)
super "Audit phase found failures - #{num_failed}/#{num_total} controls failed"
end
diff --git a/lib/chef/platform/rebooter.rb b/lib/chef/platform/rebooter.rb
index c678b60dd1..74c8b2da1f 100644
--- a/lib/chef/platform/rebooter.rb
+++ b/lib/chef/platform/rebooter.rb
@@ -19,6 +19,7 @@
require "chef/dsl/reboot_pending"
require "chef/log"
require "chef/platform"
+require "chef/application/exit_code"
class Chef
class Platform
@@ -27,6 +28,8 @@ class Chef
class << self
+ include Chef::DSL::RebootPending
+
def reboot!(node)
reboot_info = node.run_context.reboot_info
@@ -38,8 +41,16 @@ class Chef
"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)
+ msg = "Rebooting server at a recipe's request. Details: #{reboot_info.inspect}"
+ begin
+ Chef::Log.warn msg
+ shell_out!(cmd)
+ rescue Mixlib::ShellOut::ShellCommandFailed => e
+ raise Chef::Exceptions::RebootFailed.new(e.message)
+ end
+
+ raise Chef::Exceptions::Reboot.new(msg) if Chef::Application::ExitCode.enforce_rfc_062_exit_codes?
+ Chef::Application::ExitCode.notify_reboot_exit_code_deprecation
end
# this is a wrapper function so Chef::Client only needs a single line of code.