Using Built-in Chef Classes to Learn about Your Chef Environment
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.