class Chef::Handler::Sns

Chef Handler SNS main class.

A simple Chef report handler that reports status of a Chef run through [Amazon SNS](aws.amazon.com/sns/), [including IAM roles support](usage-with-amazon-iam-roles).

Chef Handler SNS main class.

A simple Chef report handler that reports status of a Chef run through [Amazon SNS](aws.amazon.com/sns/), [including IAM roles support](usage-with-amazon-iam-roles).

Constants

VERSION

chef-handler-sns Ruby Gem version.

Public Class Methods

new(config = {}) click to toggle source

Constructs a new `Sns` object.

@example `/etc/chef/client.rb` Configuration Example

require 'chef/handler/sns'
sns_handler = Chef::Handler::Sns.new
sns_handler.access_key '***AMAZON-KEY***'
sns_handler.secret_key '***AMAZON-SECRET***'
sns_handler.topic_arn 'arn:aws:sns:***'
sns_handler.region 'us-east-1' # optional
exception_handlers << sns_handler

@example `/etc/chef/client.rb` Example Using a Hash for Configuration

require 'chef/handler/sns'
exception_handlers << Chef::Handler::Sns.new(
  access_key: '***AMAZON-KEY***',
  secret_key: '***AMAZON-SECRET***',
  topic_arn: 'arn:aws:sns:***',
  region: 'us-east-1' # optional
)

@example `/etc/chef/client.rb` Using IAM Roles

require 'chef/handler/sns'
exception_handlers << Chef::Handler::Sns.new(
  topic_arn: 'arn:aws:sns:us-east-1:12341234:MyTopicName'
)

@example Using the `chef_handler` Cookbook

# Install the `chef-handler-sns` RubyGem during the compile phase
chef_gem 'chef-handler-sns' do
  compile_time true # Only for Chef 12
end
# Then activate the handler with the `chef_handler` LWRP
chef_handler 'Chef::Handler::Sns' do
  source 'chef/handler/sns'
  arguments(
    access_key: '***AMAZON-KEY***',
    secret_key: '***AMAZON-SECRET***',
    topic_arn: 'arn:aws:sns:***'
  )
  supports exception: true
  action :enable
end

@example Using the `chef-client` Cookbook

node.default['chef_client']['config']['exception_handlers'] = [{
  'class' => 'Chef::Handler::Sns',
  'arguments' => {
    access_key: '***AMAZON-KEY***',
    secret_key: '***AMAZON-SECRET***',
    topic_arn: 'arn:aws:sns:***'
  }.map { |k, v| "#{k}: #{v.inspect}" }
}]

@param config [Hash] Configuration options.

@option config [String] :access_key AWS access key (required, but will

try to read it from Ohai with IAM roles).

@option config [String] :secret_key AWS secret key (required, but will

try to read it from Ohai with IAM roles).

@option config [String] :token AWS security token (optional, read from

Ohai with IAM roles). Set to `false` to disable the token detected by
Ohai.

@option config [String] :topic_arn AWS topic ARN name (required). @option config [String] :region AWS region (optional). @option config [String] :subject Message subject string in erubis

format (optional).

@option config [String] :body_template Full path of an erubis template

file to use for the message body (optional).

@option config [Array] :filter_opsworks_activities An array of

OpsWorks activities to be triggered with (optional). When set,
everything else will be discarded.

@api public

# File lib/chef/handler/sns.rb, line 119
def initialize(config = {})
  Chef::Log.debug("#{self.class} initialized.")
  config_init(config)
end

Public Instance Methods

report() click to toggle source

Send a SNS report message.

This is called by Chef internally.

@return void

@api public

# File lib/chef/handler/sns.rb, line 133
def report
  config_check(node)
  return unless allow_publish(node)
  sns.publish(
    topic_arn: topic_arn,
    message: sns_body,
    subject: sns_subject,
    message_structure: message_structure
  )
end

Protected Instance Methods

allow_publish(node) click to toggle source

Checks if the message will be published based in configured OpsWorks activities.

@param node [Chef::Node] Chef Node that contains the activities.

@return [Boolean] Whether the message needs to be sent.

@api private

# File lib/chef/handler/sns.rb, line 156
def allow_publish(node)
  return true if filter_opsworks_activity.nil?

  if node.attribute?('opsworks') &&
     node['opsworks'].attribute?('activity')
    return filter_opsworks_activity.include?(node['opsworks']['activity'])
  end

  Chef::Log.debug(
    'You supplied opsworks activity filters, but node attr was not '\
    'found. Returning false'
  )
  false
end
default_sns_subject() click to toggle source

Returns the SNS subject used by default.

@return [String] The SNS subject.

@api private

# File lib/chef/handler/sns.rb, line 241
def default_sns_subject
  chef_client = Chef::Config[:solo] ? 'Chef Solo' : 'Chef Client'
  status = run_status.success? ? 'success' : 'failure'
  fix_subject_encoding("#{chef_client} #{status} in #{node.name}"[0..99])
end
fix_body_encoding(o) click to toggle source

Fixes the encoding of SNS bodies.

@param o [String, Object] The body to fix.

@return [String] The message fixed.

@api private

# File lib/chef/handler/sns.rb, line 230
def fix_body_encoding(o)
  fix_encoding(o, 'UTF-8')
end
fix_encoding(o, encoding) click to toggle source

Fixes or forces the correct encoding of strings.

Replaces wrong characters with `'?'`s.

@param o [String, Object] The string to fix. @param encoding [String] The encoding to use.

@return [String] The message fixed.

@api private

# File lib/chef/handler/sns.rb, line 200
def fix_encoding(o, encoding)
  encode_opts = { invalid: :replace, undef: :replace, replace: '?' }

  return o.to_s.encode(encoding, encode_opts) if RUBY_VERSION >= '2.1.0'
  # Fix ArgumentError: invalid byte sequence in UTF-8 (issue #7)
  o.to_s.encode(encoding, 'binary', encode_opts)
end
fix_subject_encoding(o) click to toggle source

Fixes the encoding of SNS subjects.

@param o [String, Object] The subject to fix.

@return [String] The message fixed.

@api private

# File lib/chef/handler/sns.rb, line 217
def fix_subject_encoding(o)
  fix_encoding(o, 'ASCII')
end
limit_utf8_size(str, size) click to toggle source

Limits the size of a UTF-8 string in bytes without breaking it.

Based on stackoverflow.com/questions/12536080/ ruby-limiting-a-utf-8-string-by-byte-length

@param str [String] The string to limit. @param size [Integer] The string size in bytes.

@return [String] The final string.

@note This code does not work properly on Ruby `< 2.1`.

@api private

# File lib/chef/handler/sns.rb, line 262
def limit_utf8_size(str, size)
  # Start with a string of the correct byte size, but with a possibly
  # incomplete char at the end.
  new_str = str.byteslice(0, size)

  # We need to force_encoding from utf-8 to utf-8 so ruby will
  # re-validate (idea from halfelf).
  until new_str[-1].force_encoding('utf-8').valid_encoding?
    # Remove the invalid char
    new_str = new_str.slice(0..-2)
  end
  new_str
end
sns() click to toggle source

Returns the {Aws::SNS} object used to send the messages.

@return [Aws::SNS::Client] The SNS client.

# File lib/chef/handler/sns.rb, line 175
def sns
  @sns ||= begin
    params = {
      access_key_id: access_key,
      secret_access_key: secret_key,
      logger: Chef::Log
    }
    params[:region] = region if region
    params[:session_token] = token if token
    Aws::SNS::Client.new(params)
  end
end
sns_body() click to toggle source

Generates the SNS body.

@return [String] The body string.

@api private

# File lib/chef/handler/sns.rb, line 297
def sns_body
  template = IO.read(body_template ||
    "#{File.dirname(__FILE__)}/sns/templates/body.erb")
  context = self
  eruby = Erubis::Eruby.new(fix_body_encoding(template))
  body = fix_body_encoding(eruby.evaluate(context))
  limit_utf8_size(body, 262_144)
end
sns_subject() click to toggle source

Generates the SNS subject.

@return [String] The subject string.

@api private

# File lib/chef/handler/sns.rb, line 283
def sns_subject
  return default_sns_subject unless subject
  context = self
  eruby = Erubis::Eruby.new(fix_subject_encoding(subject))
  fix_subject_encoding(eruby.evaluate(context))[0..99]
end