Showing posts with label chef error handling. Show all posts
Showing posts with label chef error handling. Show all posts

Wednesday, June 3, 2020

Using Chef Event Handlers for Resources

Chef Provides a comprehensive list of events and different ways to handle them. These are decently documented at https://docs.chef.io/handlers.
In a recent use case, I had to ensure that a folder has only the files I wanted it to have, any more files or directories should get deleted whenever the chef-client runs. Similarly, if a file is missing, it should be re-created. In addition, I must know the names of the files that were created/deleted and the name and the content of the file that was updated.

Problem 1 - How to find which files you don't want?
There's no easy way chef provides this, so a bit of Ruby code to the rescue.
# The path where all my source files are stored.
# For me the source is the cookbook itself.
source_path = "#{__dir__}/../files/default/myfiles"

# Path on Target; where I need all my files
# Set some temporary variables
target_files = []

# Deleted Files
deleted_files = []

# updated_files
updated_files = {}

# The code to populate our source files dynamically
Dir.chdir(source_path) do
  source_files = Dir.glob("*")
end

# Ensure Target Directory Exists
directory target_path
# List of Files on Target
begin
  Dir.chdir(target_path) do
    target_files = Dir.glob("*")
  end
rescue
  Chef::Log.info "Directory may not exists, skipping as non-fatal"
end

files_to_delete = target_files - source_files
Chef::Log.info "Total Source Files: #{source_files.length}"
Chef::Log.info "Total Target Files: #{target_files.length}"
Chef::Log.info "Files to delete: #{files_to_delete}"
Problem 2 - How to find which files were created/deleted/modified?
This is where the Chef handler events kick in. I prefer using the 'on event' style.
# Add the Resource Handler
Chef.event_handler do
 on :resource_updated do |resource, action, update|
   begin
     if resource.is_a?(Chef::Resource::File) or resource.is_a?(Chef::Resource::CookbookFile) or resource.is_a?(Chef::Resource::Directory)
       if action == :delete
         deleted_files << resource.path
       elsif action == :create
         updated_files.store(resource.path, resource.diff)
       end
     end
   rescue
     # do nothing
   end
  end
end
The code above will get triggered everytime a resource is updated (any resource). Chef provides different events which you can trap, here I was interested to know only about the updates. I was also only interested in if the resource was a File or a Directory.

Bringing the last bits together.
Once I knew which files to delete, I could just delete them and the event above would keep a track of which files were deleted. Similarly for file creation and modification.
files_to_delete.each do | f |
  if File.directory?("#{target_path}/#{f}")
    directory "#{target_path}/#{f}" do
      recursive true
      action :delete
    end
  else
    file "#{target_path}/#{f}" do
      action :delete
    end
  end
end
source_files.each do | f |
  cookbook_file "#{target_path}/#{f}" do
    source "#{f}"
  end
end

# Finally, log a message with all files deleted.
# Notice the use of lazy block to evaluate the variable at converge time.
if !deleted_files.empty?
  log 'deleted_files' do
    message lazy { "Deleted Files: #{deleted_files}" }
  end
end

if !updated_files.empty?
  log 'updated_files' do
    message lazy { "Updated Files: #{updated_files}" }
  end
end

ruby_block 'updated_status' do
  block do
    node.normal['deleted_files'] =  deleted_files
    node.normal['updated_files'] =  updated_files
    node.normal['last_run_time'] =  Time.now
  end
end