1
votes

I am trying to understand the Chef documentation on Custom Handlers.

The steps seem easy enough but I still don't understand how it works.

  1. Download the chef_handler cookbook
  2. Create a custom handler
  3. Write a recipe using the chef_handler resource
  4. Add that recipe to a node’s run-list, often as the first recipe in that run-list

Step 1 is no longer necessary as chef_handler cookbook is now part of Chef.

Step 2 creates a handler. The example given is

require 'net/smtp'

module OrgName
  class SendEmail < Chef::Handler
    def report
      if run_status.failed? then
        message  = "From: sender_name <[email protected]>\n"
        message << "To: recipient_address <[email protected]>\n"
        message << "Subject: chef-client Run Failed\n"
        message << "Date: #{Time.now.rfc2822}\n\n"
        message << "Chef run failed on #{node.name}\n"
        message << "#{run_status.formatted_exception}\n"
        message << Array(backtrace).join('\n')
        Net::SMTP.start('your.smtp.server', 25) do |smtp|
          smtp.send_message message, 'sender@example', 'recipient@example'
        end
      end
    end
  end
end

Now comes step 3 which I don't understand, add the following to a recipe

send_email 'blah' do
  # recipe code
end

When I run my recipe it just produces the error message which I expected to begin with:

FATAL: NoMethodError: undefined method `send_email` for cookbook: test, recipe: default :Chef::Recipe

How is this supposed to work? Are there other simple but working examples of custom handlers?

For Chef 15.0.300

2

2 Answers

2
votes

The documentation on Chef handlers is a little unclear indeed at first look. Looking into more details, one thing is clear. chef_handler is a Chef resource (since Chef client 14). It supports two handlers by default - JsonFile and ErrorReport.

There is more documentation on custom handlers. This shows that we need to have some chef_gem installed for the custom handler(s) to work (apart from the above two).

Simple Email

A handler that collects exception and report handler data and then uses pony to send email reports that are based on Erubis templates.

Connecting the dots... then we should have a recipe that installs the chef_gem and enables the handler. I created a separate cookbook, which can be reused by other cookbooks.

In the below example, I have a cookbook my_handlers:

recipes/json.rb
recipes/error_report.rb
recipes/email.rb
libraries/send_mail.rb

In my_handlers/recipes/email.rb:

chef_gem 'chef-handler-mail'

chef_handler 'MailHandler' do
  source 'chef/handler/mail'
  arguments :to_address => 'root'
  action :enable
end

In my_handlers/libraries/send_mail.rb:

require 'net/smtp'

module HandlerSendEmail
  class Helper

    def send_email_on_run_failure(node_name)
      message = "From: Chef <[email protected]>\n"
      message << "To: Grant <[email protected]>\n"
      message << "Subject: Chef run failed\n"
      message << "Date: #{Time.now.rfc2822}\n\n"
      message << "Chef run failed on #{node_name}\n"
      Net::SMTP.start('your.smtp.server', 25) do |smtp|
        smtp.send_message message, '[email protected]', '[email protected]'
      end
    end
  end
end

Now I have something I can reuse in other cookbooks and choose the handler I want to enable.

Let's say I have a cookbook cookbook1 in which I want to use the email handler on failure:

In cookbook1/metadata.rb:

depends 'my_handlers'

In recipes/default.rb:

# Enable the Chef email handler by calling "my_handler"
include_recipe 'my_handlers::email'

Chef.event_handler do
  on :run_failed do
    HandlerSendEmail::Helper.new.send_email_on_run_failure(Chef.run_context.node.name)
  end
end

# Usual resource declarations

ruby_block 'fail the run' do
  block do
    fail 'deliberately fail the run'
  end
end

If failure occurs (deliberately as above), Chef email handler is invoked and triggers an email indicating failure.

0
votes

I think step 3 is wrong in documentation. It should be like this:

# recipe:
chef_handler 'OrgName::SendEmail' do # Full class name
  # relative path to your handler
  source ::File.expandpath('../../files/default/handlers/send_mail.rb', __FILE__)
  arguments() # Hash or Array to pass to handler constructor
end

It is common to store handlers in files/default/handlers/ directory in cookbook, that's why the source attribute is like this. arguments is not required in your case as your initialize method in handler does not expect any parameters.