56

Under certain conditions, I need to abort/end a Chef run with a non-zero status code, that will then propagate back through our deployment chain and eventually to Jenkins resulting in a big, fat red ball.

What is the best way to do this?

Jordan Dea-Mattson
  • 5,695
  • 5
  • 35
  • 52

5 Answers5

117

For the readers coming to this question and answer in the future that may not be familiar with Chef, a Chef run "converges" the node, or brings it in line with the policy declared in the recipe(s) it is running. This is also called "convergence." This has two phases, "compile" and "execute." The compile phase is when Chef evaluates ("compiles") the recipes' Ruby code, looking for resources to add to the Resource Collection. Once that is complete, it "executes" the actions for each resource to put it into the state desired. System commands are run, etc.

Erik Hollensbe wrote an excellent walk through of how this works in 2013.

Now, for the answer:

There are several ways to end a Chef run, or exit a Chef recipe, depending on how you want to go about it, since Chef recipes are Ruby code.

If your goal is to stop processing a recipe based on a condition, but continue with the rest of the run, then use the return Ruby keyword. For example:

file '/tmp/ponies' do
  action :create
end

return if platform?('windows')

package 'bunnies-and-flowers' do
  action :install
end

We presume that if the system is Windows, it doesn't have a package manager that can install the bunnies-and-flowers package, so we return from whence we came.

If you wish to abort the Chef run entirely

Tl;dr: Use raise. It's the best practice to abort a Chef run in case of an error condition.

That said, chef-client exits if it encounters an unhandled exception anywhere in the run. For example, if a template resource can't find its source file, or if the user running chef-client doesn't have permission to do something like make a directory. This is why using raise also works to end a run.

Where you put raise matters. If you use it in a ruby_block resource, it will only raise during the execution phase in convergence. If you use it outside of a resource like the return example above, it will happen during the compile phase.

file '/tmp/ponies' do
  action :create
end

raise if platform?('windows')

package 'bunnies-and-flowers' do
  action :install
end

Perhaps we do have a package manager on Windows, and we want this package installed. The raise will result in Chef fatally exiting and giving a stack trace.

In years past, another approach was to use Chef::Application.fatal! - as written by me in this answer. Times have changed and this is NOT RECOMMENDED. Do not do this anymore. If you're doing it, switch to raise and as mentioned, write your own exception handler if your needs are more complicated (see below).

More Graceful error handling

Since recipes are Ruby, you can also gracefully handle error conditions with a begin..rescue block.

begin
  dater = data_bag_item(:basket, "flowers")
rescue Net::HTTPServerException
  # maybe some retry code here?
  raise "Couldn't find flowers in the basket, need those to continue!"
end

data_bag_item makes an HTTP request for a data bag on the Chef Server, and will return a Net::HTTPServerException if there's a problem from the server (404 not found, 403 unauthorized, etc). We could possibly attempt to retry or do some other handling, and then fall back to raise.

Reporting Errors

Simply exiting and tossing a stack trace is fine if you're running Chef from the command-line. However, if you're running it in cron or as a daemon across a few, or even dozens or hundreds of machines, this isn't a great way to keep sanity when there's problems.

Enter Chef's report/exception handler feature. You can use a handler for your Chef runs. All report handlers are run at the end of a Chef run. Exception handlers are run at the end of an aborted Chef run. The status of the run is tracked, and can be checked in the handler, so you can write one that handles both kinds of run (successful/completed or unsuccessful/aborted).

The documentation tells you how to write one. It also includes a list of available open source handlers that you can use for a variety of services, including:

  • Email over SMTP
  • IRC
  • Graphite
  • HipChat

And many more.

jtimberman
  • 8,208
  • 2
  • 41
  • 37
  • 3
    Awesome answer! Very helpful. I had looked for this in the various Chef resources, but didn't find it. If I had, this Question wouldn't exist. Recently, I decided that when I searched for the answer to a question and didn't find it either on Stackoverflow or easily from expected resources, that once I had an answer I would put together a Stackoverflow Question and Answer pair to capture what I learned. – Jordan Dea-Mattson Jan 13 '13 at 19:01
  • 1
    I have never been able to get `rescue Net::HTTPServerException` to work. It does not rescue the exception. – Zabba Sep 19 '13 at 05:26
  • 2
    loved the spanish inquisition comment :-) – Mamun Feb 22 '16 at 15:55
  • so that's why it's called "converge"! – wisbucky Mar 04 '16 at 22:25
  • 1
    I added a strikethrough to the `Chef::Application.fatal!` approach. This approach MUST not be used by chef users, that is an internal API which eventually causes bugs. See for example: https://github.com/chefspec/chefspec/issues/489#issuecomment-53592637 – lamont May 01 '19 at 20:03
  • I've updated the answer to remove the `Chef::Application.fatal!` approach :). Thanks, Lamont! – jtimberman May 01 '19 at 21:51
8

The recommended way to abort or edit a Chef run is to raise an exception. Here's an example:

ruby_block "some tricky operation" do
  block do
    OperationFoo
    raise "Operation Foo Failed" if some_condition
  end
end
gene_wood
  • 1,630
  • 2
  • 24
  • 33
Jordan Dea-Mattson
  • 5,695
  • 5
  • 35
  • 52
2

Chef::Application.fatal! should do what your looking for. Here is an example from our code base which might be helpful.

cipher = case key.length
    when 16 then "AES-128-ECB"
    when 24 then "AES-192-ECB"
    when 32 then "AES-256-ECB"
else
    Chef::Application.fatal!("AES Key must be 16, 24, or 32 characters in length but key #{key} has length of #{key.length}")
end
John T Dyer
  • 146
  • 3
  • 4
  • 1
    As a Core Chef developer for the past 8 years, please never use `Chef::Application.fatal!` in cookbook or recipe code. See for example: https://github.com/chefspec/chefspec/issues/489#issuecomment-53592637 We also routinely encounter reporting issues with exception handling caused by calling `fatal!` directly. Those issues we cannot accept as bugs, will not be fixed, and this approach MUST not be used. Rules will soon be added to foodcritic to warn against its usage. You can consider this an official company recommendation. – lamont May 01 '19 at 19:55
  • @lamont is there any other way that could exit recipe and tell post handler that recipe failed except raise? I donot want to use raise becaues raise will show whole stacktrace include source codes. – NewBee Jun 07 '19 at 10:11
  • I think you'd need to use a custom output formatter to prevent the display of the whole rendered error message. – lamont Jun 07 '19 at 16:15
-2

Just use below statement when you want chef to finish after some action:

throw :end_client_run_early

It will exit without any error.

סטנלי גרונן
  • 2,740
  • 21
  • 43
  • 62
-5

To do an unclean exit during a chef-solo run, try this:

bash 'exit' do
    code 'killall -9 chef-solo'
end
dan
  • 195
  • 1
  • 5