class SousVide::Handler

SousVide exposes minimal API as it's driven by events passed from chef-client.

It's sufficient to enable it and with default configuration it will print summary to chef-client logs.

Collected data can be sent to different outputs, ie. HTTP endpoint.

:run_name and :run_id can be customized, it will be passed to the configured output along all data.

@note You should interact with SousVide via top level methods, i.e. SousVide.run_id = '1123'.

@example Enable SousVide with JSON HTTP output and custom run name

ruby_block "enable SousVide" do
  block do
    json_http_output = SousVide::Outputs::JsonHTTP.new(url: "http://localhost:3000")
    SousVide::Handler.instance.run_name = "custom run name"
    SousVide::Handler.instance.sous_output = json_http_output
    SousVide::Handler.register(node.run_context)
  end
  action :nothing
end.run_action(:run)

@see SousVide::Handler.register @see SousVide::Handler#sous_output @see www.rubydoc.info/gems/chef/Chef/EventDispatch/Base

Attributes

chef_run_context[W]

Chef::RunContext object from chef-client. @see www.rubydoc.info/gems/chef/Chef/RunContext @api private

logger[RW]

Logger object SousVide will use (default Chef::Log)

Event methods emit debug level messages and it can be helpful to configure a dedicated logger for SousVide to avoid excessive messages from chef-client.

@example Configure custom debug logger

logger = Logger.new(STDOUT)
logger.level = Logger::DEBUG
@sous_vide.logger = logger

@see www.rubydoc.info/gems/chef/Chef/Log

processed[R]

@return [Array<SousVide::TrackedResource>] a list of processed resources. @api private

processing_now[R]

@return [SousVide::TrackedResource] a resource SousVide is currently processing. @api private

run_id[RW]

SousVide custom run ID. It will be included in the output, not used otherwise.

It is not chef client run id.

@return [String]

run_name[RW]

SousVide custom run name. It will be included in the output, not used otherwise.

@return [String]

run_phase[R]

Chef-client run phase. One of:

  • compile

  • converge

  • delayed

  • post-converge

Compile & converge phases reflect standard Chef two-pass model. Delayed & post-converge are custom phases added by SousVide.

SousVide enters delayed phase when chef-client begins processing delayed notifications, that is after all resources in the run list have bggen converged.

Post-converge phase happens when chef-client run fails and aborts the converge process. It will include resources that should, but were not converged.

@api private

@return [String]

sous_output[RW]

An object SousVide will pass all collected data to at the end of the chef-client run.

The output must respond to call (like Proc).

Defaults to SousVide::Outputs::Logger.

@example Configure JsonHTTP output

json_http_output = SousVide::Outputs::JsonHTTP.new(url: "http://localhost:3000")
@sous_vide.sous_output = json_http_output

@example Configure multiple outputs

json_file_output = SousVide::Outputs::JsonFile.new
logger_output = SousVide::Outputs::Logger.new
multi_output = SousVide::Outputs::Multi.new(json_file_output, logger_output)
@sous_vide.sous_output = multi_output

@see SousVide::Outputs::JsonFile @see SousVide::Outputs::JsonHTTP @see SousVide::Outputs::Logger @see SousVide::Outputs::Multi

Public Class Methods

new() click to toggle source
# File lib/sous_vide/handler.rb, line 152
def initialize
  @execution_order = 0
  @resource_collection_cursor = 0

  @processed = []
  @processing_now = nil

  @run_started_at = Time.now.strftime("%F %T")

  # Not related to RunStatus#run_id, it's SousVide internal run ID.
  @run_id = SecureRandom.uuid.split("-").first # => 596e9d00
  @run_phase = "compile"

  @logger = ::Chef::Log
  @sous_output = Outputs::Logger.new(logger: @logger)
end
register(chef_run_context) click to toggle source

Enables SousVide. It can be called anywhere in the recipe, ideally as early as possible.

All converge-time resources will be included in the report regardless at what point registration happens.

Compile-time resources defined before registration will not be included.

@note chef_handler resource does not support subscribing to :events. Chef.event_handler DSL

could be used but dealing with returns and exceptions in blocks is tricky. Using Chef API
and Ruby feels the most reliable.

@todo see client.rb start_handlers as an option.

@example

SousVide::Handler.register(node.run_context)

@return (void)

# File lib/sous_vide/handler.rb, line 143
def self.register(chef_run_context)
  ::Chef::Log.info "Registering SousVide"

  instance.chef_run_context = chef_run_context
  instance.populate_node_data

  chef_run_context.events.register(instance)
end

Public Instance Methods

node_data() click to toggle source

Node related attributes

@return (Hash)

# File lib/sous_vide/handler.rb, line 185
def node_data
  {
    chef_node_ipv4: @chef_node_ipv4,
    chef_node_instance_id: @chef_node_instance_id,
    chef_node_role: @chef_node_role
  }
end
populate_node_data() click to toggle source

Populates node attributes from Chef run context. These attributes will be passed to the configured output as :node_data.

@api private

# File lib/sous_vide/handler.rb, line 197
def populate_node_data
  @chef_node_ipv4 = @chef_run_context.node["ipaddress"] || "<no ip>"
  @chef_node_role = @chef_run_context.node["roles"].first || "<no role>"
  @chef_node_instance_id = @chef_run_context.node.name
end
run_data() click to toggle source

Chef-client run related attributes.

@return (Hash)

# File lib/sous_vide/handler.rb, line 172
def run_data
  {
    chef_run_id: @run_id,
    chef_run_name: @run_name,
    chef_run_started_at: @run_started_at,
    chef_run_completed_at: @run_completed_at,
    chef_run_success: @run_success
  }
end

Private Instance Methods

consume_unprocessed_resources!() click to toggle source

Backfills unprocessed resources. This is called only if chef-client failed (:converge_failed event). SousVide is now in “post-converge” run phase.

Unprocessed resources as a result of notifications are not included.

# File lib/sous_vide/handler.rb, line 246
def consume_unprocessed_resources!
  all_known_resources = expand_chef_resources!

  # No unprocessed resources left. Failure likely occured on last resource or in a delayed
  # notification.
  # TODO: check delayed notification failure. We may also want to backfeed unprocessed delayed
  # notifications too? Maybe.
  return if @resource_collection_cursor >= all_known_resources.size

  unprocessed = all_known_resources[@resource_collection_cursor..-1]

  # Pass unprocessed resources via  :resource_action_start and :resource_completed so
  # they will end up in @processed collection, but with status set to "unprocessed".
  unprocessed.each do |tracked|
    resource_action_start(
      tracked.chef_resource_handle, # new_resource
      tracked.action,               # action
      nil,                          # notification_type
      nil                           # notifying_resource
    )

    resource_completed(
      tracked.chef_resource_handle  # new_resource
    )
  end
end
create(chef_resource:, action:) click to toggle source

Creates SousVide::TrackedResource from Chef resource and action.

@param chef_resource [Chef::Resource] @param action [Symbol] an action chef-client executes on the resource

@return (SousVide::TrackedResource)

# File lib/sous_vide/handler.rb, line 218
def create(chef_resource:, action:)
  tracked = TrackedResource.new(action: action,
                                name: chef_resource.name,
                                type: chef_resource.resource_name)

  tracked.cookbook_name = chef_resource.cookbook_name || "<Dynamically Defined Resource>"
  tracked.cookbook_recipe = chef_resource.recipe_name || "<Dynamically Defined Resource>"
  tracked.source_line = chef_resource.source_line || "<Dynamically Defined Resource>"
  tracked.chef_resource_handle = chef_resource
  tracked
end
debug(*args) click to toggle source
# File lib/sous_vide/handler.rb, line 297
def debug(*args)
  message = args.compact.join(" ")
  logger.debug(message)
end
expand_chef_resources!() click to toggle source

Expands chef-client run list. Resources with multiple actions must be expanded, ie. given resource:

service "nginx" do
  action [:enable, :start]
end

After expansion we will have 2 resources:

* service[nginx] with action :enable
* service[nginx] with action :start

On failure SousVide will pick up unprocessed resources from here to feed the handler in post-converge stage.

On a successful chef-client run this method won't be called.

# File lib/sous_vide/handler.rb, line 289
def expand_chef_resources!
  @chef_run_context.resource_collection.flat_map do |chef_resource|
    Array(chef_resource.action).map do |action|
      create(chef_resource: chef_resource, action: action)
    end
  end
end
nested?(chef_resource) click to toggle source

Tells if an event is related to the current SousVide resource. If not it's nested.

Once a top level resource triggered :resource_action_start any events not related to it will be ignored by SousVide.

@return (Boolean)

# File lib/sous_vide/handler.rb, line 237
def nested?(chef_resource)
  @processing_now &&
    @processing_now.chef_resource_handle != chef_resource
end
send_to_output!() click to toggle source

Sends all collected data to configured output.

It is called at the end of the converge process, both failure and success.

# File lib/sous_vide/handler.rb, line 208
def send_to_output!
  @sous_output.call(run_data: run_data, node_data: node_data, resources_data: @processed)
end