Using Built-in Chef Classes to Learn about Your Chef Environment

Author: Joe Gange Posted In: DevOps

One thing I’ve come to learn about Chef is there are many ways to accomplish the same task. Since I’m usually under pressure to produce a working solution quickly, I often start by Googling specific questions. I want to discover if someone has already created a recipe or a library that accomplishes the same task with minimal tweaking.

Of course, using other people’s code brings distinct challenges and risks. Do I really understand what the code I’m borrowing does? How much time do I want to spend trying to figure that out?

Rolling your own code also presents its own obstacles. If I had to write the code myself, I’d have to work through the usual development process of learn, code, test, correct, repeat.

Fortunately, I realized there is a middle ground between tweaking someone else’s code and writing my own.

Digging into the Source Code

The Chef developers chose to write Chef in Ruby, an interpreted object-oriented language. As someone who likes to peek under the covers, I took some time to gain a working knowledge of Ruby after I started using Chef.

In this case, I needed to provide usage data about a client’s Chef environment. Fortunately, I was able to quickly scan the code for some promising leads because the source code for Chef is available on Github.

Specifically, I needed to report on cookbooks by version, existing databags and configured environments. If I was doing this interactively, I could write some queries using knife and get the information I needed. But the client wanted this data as part of a larger set of validations in the form of a recipe.

After poking around Git, I quickly realized the information I was looking for was in the lib folder (https://github.com/chef/chef/tree/master/lib/chef). Better yet, I discovered individual Ruby files included methods for the objects I needed data about.

The next challenge was to figure out how to get the information from within a recipe. Chef allows you to import external libraries (in the form of Ruby gems) during recipe execution, but what I needed was internal to Chef.

Accessing the proper method

One way to access methods from a class is to use the namespace resolution operator in Ruby. Ruby uses the following syntax to describe nested methods: <top level object>::<second level object>::<third level> and so on. I had to figure out the hierarchy to access the methods I needed.

I found the following information on how to access the methods in the comments of the cookbook_version.rb file.

# == Chef::CookbookVersion

# CookbookVersion is a model object encapsulating the data about a Chef

# cookbook. Chef supports maintaining multiple versions of a cookbook on a

# single server; each version is represented by a distinct instance of this

# class.

Specifically, I needed to list all the versions of the cookbooks loaded on the server, which led me to the list_all_versions method. I experimented with the list method but it only returned the latest version of a cookbook.

# The API returns only a single version of each cookbook in the result from the cookbooks method
  def self.list
    chef_server_rest.get("cookbooks")
  end

# Alias latest_cookbooks as list
  class << self
    alias :latest_cookbooks :list
  end

  def self.list_all_versions
    chef_server_rest.get("cookbooks?num_versions=all")
  end

Applying the internal Chef class method

The list_all_versions method returns a hash containing the all the cookbooks loaded on the Chef server by version. I extracted those fields from the hash in my library method.

The method to extract that data looked like this:

def build_cookbook_list()
  cookbooks = Chef::CookbookVersion.list_all_versions
  cookbook_report = Hash.new {|h, k| h[k] = []}
  cookbooks.each do |k,v|
    0.upto(cookbooks[k]["versions"].size-1) do |ver|
    cookbook_report[k] << cookbooks[k]["versions"][ver]["version"]
  end
end

The Chef developers also provided a list method to access information about databags and environments. You can call the method with the syntax Chef::Databags.list and Chef::Environment.list respectively.

Once I had created my methods in a helper library, the recipe code was easy to write. I used the statement cookbooks = build_cookbook_list to return the data about the cookbooks in the system.

As a bonus, I avoided having to manage external class references inside my methods. Because I was using built in Chef methods, the methods I was calling were already within the scope of the recipe.

I hope this helps you find a way to easily obtain information about your Chef environment inside a recipe.