summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteven Murawski <steven.murawski@gmail.com>2016-02-18 14:58:32 -0600
committerSteven Murawski <steven.murawski@gmail.com>2016-05-23 13:32:49 -0500
commite24b8e2cd67aef572fde184e8b07d48333184b29 (patch)
tree2b1f6507104149182791436be30561be3be1aef2
parente4bca443a3ddca6381275a1a252de04f5e4883c4 (diff)
downloadchef-e24b8e2cd67aef572fde184e8b07d48333184b29.tar.gz
* define exit codes
* exit code functional specs * audit exit codes * reboot now/reboot failed/reboot pending exit codes * Deal with forked and unforked process and get the right exit code * Reboot Now should really be reboot scheduled * pass exception rather than exit code * updated with sigint and sigterm * support legacy fatal!("", 2) behavior * fixup all fatal! and exit! calls
-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
-rw-r--r--spec/integration/client/exit_code_spec.rb245
-rw-r--r--spec/unit/application/apply_spec.rb6
-rw-r--r--spec/unit/application/exit_code_spec.rb231
-rw-r--r--spec/unit/config_fetcher_spec.rb4
13 files changed, 770 insertions, 35 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.
diff --git a/spec/integration/client/exit_code_spec.rb b/spec/integration/client/exit_code_spec.rb
new file mode 100644
index 0000000000..30020f6a3f
--- /dev/null
+++ b/spec/integration/client/exit_code_spec.rb
@@ -0,0 +1,245 @@
+
+require "support/shared/integration/integration_helper"
+require "chef/mixin/shell_out"
+require "tiny_server"
+require "tmpdir"
+require "chef/platform"
+
+describe "chef-client" do
+
+ include IntegrationSupport
+ include Chef::Mixin::ShellOut
+
+ let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..", "bin") }
+
+ # Invoke `chef-client` as `ruby PATH/TO/chef-client`. This ensures the
+ # following constraints are satisfied:
+ # * Windows: windows can only run batch scripts as bare executables. Rubygems
+ # creates batch wrappers for installed gems, but we don't have batch wrappers
+ # in the source tree.
+ # * Other `chef-client` in PATH: A common case is running the tests on a
+ # machine that has omnibus chef installed. In that case we need to ensure
+ # we're running `chef-client` from the source tree and not the external one.
+ # cf. CHEF-4914
+ let(:chef_client) { "ruby '#{chef_dir}/chef-client' --no-fork --minimal-ohai" }
+
+ let(:critical_env_vars) { %w{PATH RUBYOPT BUNDLE_GEMFILE GEM_PATH}.map { |o| "#{o}=#{ENV[o]}" } .join(" ") }
+
+ when_the_repository "does not have exit_status configured" do
+
+ def setup_client_rb
+ file "config/client.rb", <<EOM
+local_mode true
+cookbook_path "#{path_to('cookbooks')}"
+EOM
+ end
+
+ def setup_client_rb_with_audit_mode
+ file "config/client.rb", <<EOM
+local_mode true
+cookbook_path "#{path_to('cookbooks')}"
+audit_mode :audit_only
+EOM
+ end
+
+ def run_chef_client_and_expect_exit_code(exit_code)
+ shell_out!(
+ "#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'",
+ :cwd => chef_dir,
+ :returns => [exit_code])
+ end
+
+ context "has a cookbook" do
+ context "with a library" do
+ context "which cannot be loaded" do
+ before do
+ file "cookbooks/x/recipes/default.rb", ""
+ file "cookbooks/x/libraries/error.rb", "require 'does/not/exist'"
+ end
+
+ it "exits with GENERIC_FAILURE, 1" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+ end
+
+ context "with an audit recipe" do
+ context "which fails" do
+ before do
+ file "cookbooks/x/recipes/default.rb", <<-RECIPE
+control_group "control group without top level control" do
+ it "should fail" do
+ expect(2 - 2).to eq(1)
+ end
+end
+RECIPE
+ end
+
+ it "exits with GENERIC_FAILURE, 1" do
+ setup_client_rb_with_audit_mode
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+ end
+
+ context "with a recipe" do
+ context "which throws an error" do
+ before { file "cookbooks/x/recipes/default.rb", "raise 'BOOM'" }
+
+ it "exits with GENERIC_FAILURE, 1" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+
+ context "with a recipe which calls Chef::Application.fatal with a non-RFC exit code" do
+ before { file "cookbooks/x/recipes/default.rb", "Chef::Application.fatal!('BOOM', 123)" }
+
+ it "exits with the specified exit code" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 123
+ end
+ end
+
+ context "with a recipe which calls Chef::Application.exit with a non-RFC exit code" do
+ before { file "cookbooks/x/recipes/default.rb", "Chef::Application.exit!('BOOM', 231)" }
+
+ it "exits with the specified exit code" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 231
+ end
+ end
+ end
+
+ context "when an attempt to reboot fails (like from the reboot resource)" do
+ before do
+ file "cookbooks/x/recipes/default.rb", <<EOM
+raise Chef::Exceptions::RebootFailed.new
+EOM
+ end
+
+ it "exits with GENERIC_FAILURE, 1" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+ end
+ end
+
+ when_the_repository "does has exit_status enabled" do
+
+ def setup_client_rb
+ file "config/client.rb", <<EOM
+local_mode true
+cookbook_path "#{path_to('cookbooks')}"
+exit_status :enabled
+EOM
+ end
+
+ def setup_client_rb_with_audit_mode
+ file "config/client.rb", <<EOM
+local_mode true
+cookbook_path "#{path_to('cookbooks')}"
+exit_status :enabled
+audit_mode :audit_only
+EOM
+ end
+
+ def run_chef_client_and_expect_exit_code(exit_code)
+ shell_out!("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'",
+ :cwd => chef_dir,
+ :returns => [exit_code])
+ end
+
+ context "has a cookbook" do
+ context "with a library" do
+ context "which cannot be loaded" do
+ before do
+ file "cookbooks/x/recipes/default.rb", ""
+ file "cookbooks/x/libraries/error.rb", "require 'does/not/exist'"
+ end
+
+ it "exits with GENERIC_FAILURE, 1" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+ end
+
+ context "with an audit recipe" do
+ context "which fails" do
+ before do
+ file "cookbooks/x/recipes/default.rb", <<-RECIPE
+control_group "control group without top level control" do
+ it "should fail" do
+ expect(4 - 4).to eq(1)
+ end
+end
+RECIPE
+ end
+
+ it "exits with AUDIT_MODE_FAILURE, 42" do
+ setup_client_rb_with_audit_mode
+ run_chef_client_and_expect_exit_code 42
+ end
+ end
+ end
+
+ context "with a recipe" do
+ context "which throws an error" do
+ before { file "cookbooks/x/recipes/default.rb", "raise 'BOOM'" }
+
+ it "exits with GENERIC_FAILURE, 1" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+
+ context "with a recipe which calls Chef::Application.fatal with a non-RFC exit code" do
+ before { file "cookbooks/x/recipes/default.rb", "Chef::Application.fatal!('BOOM', 123)" }
+
+ it "exits with the GENERIC_FAILURE exit code, 1" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+
+ context "with a recipe which calls Chef::Application.exit with a non-RFC exit code" do
+ before { file "cookbooks/x/recipes/default.rb", "Chef::Application.exit!('BOOM', 231)" }
+
+ it "exits with the GENERIC_FAILURE exit code, 1" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 1
+ end
+ end
+
+ context "when a reboot exception is raised (like from the reboot resource)" do
+ before do
+ file "cookbooks/x/recipes/default.rb", <<EOM
+raise Chef::Exceptions::Reboot.new
+EOM
+ end
+
+ it "exits with REBOOT_SCHEDULED, 35" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 35
+ end
+ end
+
+ context "when an attempt to reboot fails (like from the reboot resource)" do
+ before do
+ file "cookbooks/x/recipes/default.rb", <<EOM
+raise Chef::Exceptions::RebootFailed.new
+EOM
+ end
+
+ it "exits with REBOOT_FAILED, 41" do
+ setup_client_rb
+ run_chef_client_and_expect_exit_code 41
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/unit/application/apply_spec.rb b/spec/unit/application/apply_spec.rb
index 6473666fbf..0af3916134 100644
--- a/spec/unit/application/apply_spec.rb
+++ b/spec/unit/application/apply_spec.rb
@@ -52,7 +52,8 @@ describe Chef::Application::Apply do
describe "when recipe is nil" do
it "should raise a fatal with the missing filename message" do
- expect(Chef::Application).to receive(:fatal!).with("No recipe file was provided", 1)
+ expect(Chef::Application).to receive(:fatal!).with("No recipe file was provided",
+ Chef::Exceptions::RecipeNotFound.new)
@app.read_recipe_file(nil)
end
end
@@ -61,7 +62,8 @@ describe Chef::Application::Apply do
allow(File).to receive(:exist?).with(@recipe_path).and_return(false)
end
it "should raise a fatal with the file doesn't exist message" do
- expect(Chef::Application).to receive(:fatal!).with(/^No file exists at/, 1)
+ expect(Chef::Application).to receive(:fatal!).with(/^No file exists at/,
+ Chef::Exceptions::RecipeNotFound.new)
@app.read_recipe_file(@recipe_file_name)
end
end
diff --git a/spec/unit/application/exit_code_spec.rb b/spec/unit/application/exit_code_spec.rb
new file mode 100644
index 0000000000..73a113e554
--- /dev/null
+++ b/spec/unit/application/exit_code_spec.rb
@@ -0,0 +1,231 @@
+#
+# 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.
+#
+
+require "chef"
+require "spec_helper"
+
+require "chef/application/exit_code"
+
+describe Chef::Application::ExitCode do
+
+ let(:exit_codes) { Chef::Application::ExitCode }
+
+ let(:valid_rfc_exit_codes) { Chef::Application::ExitCode::VALID_RFC_062_EXIT_CODES.values }
+
+ context "Validates the return codes from RFC 062" do
+
+ before do
+ allow(Chef::Config).to receive(:[]).with(:exit_status).and_return(:enabled)
+ end
+
+ it "validates a SUCCESS return code of 0" do
+ expect(valid_rfc_exit_codes.include?(0)).to eq(true)
+ end
+
+ it "validates a GENERIC_FAILURE return code of 1" do
+ expect(valid_rfc_exit_codes.include?(1)).to eq(true)
+ end
+
+ it "validates a SIGINT_RECEIVED return code of 2" do
+ expect(valid_rfc_exit_codes.include?(2)).to eq(true)
+ end
+
+ it "validates a SIGTERM_RECEIVED return code of 3" do
+ expect(valid_rfc_exit_codes.include?(3)).to eq(true)
+ end
+
+ it "validates a AUDIT_MODE_FAILURE return code of 42" do
+ expect(valid_rfc_exit_codes.include?(42)).to eq(true)
+ end
+
+ it "validates a REBOOT_SCHEDULED return code of 35" do
+ expect(valid_rfc_exit_codes.include?(35)).to eq(true)
+ end
+
+ it "validates a REBOOT_NEEDED return code of 37" do
+ expect(valid_rfc_exit_codes.include?(37)).to eq(true)
+ end
+
+ it "validates a REBOOT_FAILED return code of 41" do
+ expect(valid_rfc_exit_codes.include?(41)).to eq(true)
+ end
+ end
+
+ context "when Chef::Config :exit_status is not configured" do
+ before do
+ allow(Chef::Config).to receive(:[]).with(:exit_status).and_return(nil)
+ allow(Chef::Config).to receive(:[]).with(:treat_deprecation_warnings_as_errors).and_return(false)
+ end
+
+ it "writes a deprecation warning" do
+ warn = "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."
+ expect(Chef).to receive(:log_deprecation).with(warn)
+ expect(exit_codes.normalize_exit_code(151)).to eq(151)
+ end
+
+ it "does not modify non-RFC exit codes" do
+ expect(exit_codes.normalize_exit_code(151)).to eq(151)
+ end
+
+ it "returns DEPRECATED_FAILURE when no exit code is specified" do
+ expect(exit_codes.normalize_exit_code()).to eq(-1)
+ end
+
+ it "returns SIGINT_RECEIVED when a SIGINT is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::SigInt.new("BOOM"))).to eq(2)
+ end
+
+ it "returns SIGTERM_RECEIVED when a SIGTERM is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::SigTerm.new("BOOM"))).to eq(3)
+ end
+
+ it "returns SIGINT_RECEIVED when a deprecated exit code error is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::DeprecatedExitCode.new("BOOM"))).to eq(2)
+ end
+
+ it "returns GENERIC_FAILURE when an exception is specified" do
+ expect(exit_codes.normalize_exit_code(Exception.new("BOOM"))).to eq(1)
+ end
+
+ end
+
+ context "when Chef::Config :exit_status is configured to not validate exit codes" do
+ before do
+ allow(Chef::Config).to receive(:[]).with(:exit_status).and_return(:disabled)
+ allow(Chef::Config).to receive(:[]).with(:treat_deprecation_warnings_as_errors).and_return(false)
+ end
+
+ it "does not write a deprecation warning" do
+ warn = "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."
+ expect(Chef).not_to receive(:log_deprecation).with(warn)
+ expect(exit_codes.normalize_exit_code(151)).to eq(151)
+ end
+
+ it "does not modify non-RFC exit codes" do
+ expect(exit_codes.normalize_exit_code(151)).to eq(151)
+ end
+
+ it "returns DEPRECATED_FAILURE when no exit code is specified" do
+ expect(exit_codes.normalize_exit_code()).to eq(-1)
+ end
+
+ it "returns GENERIC_FAILURE when an exception is specified" do
+ expect(exit_codes.normalize_exit_code(Exception.new("BOOM"))).to eq(1)
+ end
+
+ it "returns SUCCESS when a reboot is pending" do
+ allow(Chef::DSL::RebootPending).to receive(:reboot_pending?).and_return(true)
+ expect(exit_codes.normalize_exit_code(0)).to eq(0)
+ end
+
+ it "returns SIGINT_RECEIVED when a SIGINT is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::SigInt.new("BOOM"))).to eq(2)
+ end
+
+ it "returns SIGTERM_RECEIVED when a SIGTERM is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::SigTerm.new("BOOM"))).to eq(3)
+ end
+
+ it "returns SIGINT_RECEIVED when a deprecated exit code error is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::DeprecatedExitCode.new("BOOM"))).to eq(2)
+ end
+ end
+
+ context "when Chef::Config :exit_status is configured to validate exit codes" do
+ before do
+ allow(Chef::Config).to receive(:[]).with(:exit_status).and_return(:enabled)
+ allow(Chef::Config).to receive(:[]).with(:treat_deprecation_warnings_as_errors).and_return(false)
+ end
+
+ it "does write a deprecation warning" do
+ warn = "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."
+ expect(Chef).to receive(:log_deprecation).with(warn)
+ expect(exit_codes.normalize_exit_code(151)).to eq(1)
+ end
+
+ it "returns a GENERIC_FAILURE for non-RFC exit codes" do
+ expect(exit_codes.normalize_exit_code(151)).to eq(1)
+ end
+
+ it "returns GENERIC_FAILURE when no exit code is specified" do
+ expect(exit_codes.normalize_exit_code()).to eq(1)
+ end
+
+ it "returns SIGINT_RECEIVED when a SIGINT is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::SigInt.new("BOOM"))).to eq(2)
+ end
+
+ it "returns SIGTERM_RECEIVED when a SIGTERM is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::SigTerm.new("BOOM"))).to eq(3)
+ end
+
+ it "returns GENERIC_FAILURE when a deprecated exit code error is received" do
+ expect(exit_codes.normalize_exit_code(Chef::Exceptions::DeprecatedExitCode.new("BOOM"))).to eq(1)
+ end
+
+ it "returns GENERIC_FAILURE when an exception is specified" do
+ expect(exit_codes.normalize_exit_code(Exception.new("BOOM"))).to eq(1)
+ end
+
+ it "returns AUDIT_MODE_FAILURE when there is an audit error" do
+ audit_error = Chef::Exceptions::AuditError.new("BOOM")
+ runtime_error = Chef::Exceptions::RunFailedWrappingError.new(audit_error)
+ expect(exit_codes.normalize_exit_code(runtime_error)).to eq(42)
+ end
+
+ it "returns REBOOT_SCHEDULED when there is an reboot requested" do
+ reboot_error = Chef::Exceptions::Reboot.new("BOOM")
+ runtime_error = Chef::Exceptions::RunFailedWrappingError.new(reboot_error)
+ expect(exit_codes.normalize_exit_code(runtime_error)).to eq(35)
+ end
+
+ it "returns REBOOT_FAILED when the reboot command fails" do
+ reboot_error = Chef::Exceptions::RebootFailed.new("BOOM")
+ runtime_error = Chef::Exceptions::RunFailedWrappingError.new(reboot_error)
+ expect(exit_codes.normalize_exit_code(runtime_error)).to eq(41)
+ end
+
+ it "returns REBOOT_NEEDED when a reboot is pending" do
+ reboot_error = Chef::Exceptions::RebootPending.new("BOOM")
+ runtime_error = Chef::Exceptions::RunFailedWrappingError.new(reboot_error)
+ expect(exit_codes.normalize_exit_code(runtime_error)).to eq(37)
+ end
+
+ it "returns SIGINT_RECEIVED when a SIGINT is received." do
+ sigint_error = Chef::Exceptions::SigInt.new("BOOM")
+ runtime_error = Chef::Exceptions::RunFailedWrappingError.new(sigint_error)
+ expect(exit_codes.normalize_exit_code(runtime_error)).to eq(2)
+ end
+
+ it "returns SIGTERM_RECEIVED when a SIGTERM is received." do
+ sigterm_error = Chef::Exceptions::SigTerm.new("BOOM")
+ runtime_error = Chef::Exceptions::RunFailedWrappingError.new(sigterm_error)
+ expect(exit_codes.normalize_exit_code(runtime_error)).to eq(3)
+ end
+ end
+
+end
diff --git a/spec/unit/config_fetcher_spec.rb b/spec/unit/config_fetcher_spec.rb
index 35cf27f2af..6847ee5fd3 100644
--- a/spec/unit/config_fetcher_spec.rb
+++ b/spec/unit/config_fetcher_spec.rb
@@ -58,7 +58,7 @@ describe Chef::ConfigFetcher do
and_return(invalid_json)
expect(Chef::Application).to receive(:fatal!).
- with(invalid_json_error_regex, 2)
+ with(invalid_json_error_regex, Chef::Exceptions::DeprecatedExitCode.new)
fetcher.fetch_json
end
end
@@ -104,7 +104,7 @@ describe Chef::ConfigFetcher do
with("").and_return(invalid_json)
expect(Chef::Application).to receive(:fatal!).
- with(invalid_json_error_regex, 2)
+ with(invalid_json_error_regex, Chef::Exceptions::DeprecatedExitCode.new)
fetcher.fetch_json
end
end