Guide to Writing Chef Cookbooks

I wanted smartmontools installed to monitor the disk health of my LAN server at home. This is not an uncommon thing to want to do, so I thought I’d write and share a Chef cookbook for it. I also took this opportunity to write up the experience so I can illustrate how easy it is to write a cookbook for Chef.

The first thing to do when writing a cookbook is to create the cookbook directory structure with knife cookbook create. This command will create a README.rdoc by default, and I prefer Markdown, so I specify the -r md option.

knife cookbook create smartmontools -r md

By default, metadata and the default recipe are created with boilerplate content for author and copyright. I have configured the values in my knife.rb:

cookbook_copyright "Joshua Timberman"
cookbook_license   "apachev2"
cookbook_email     ""

The resulting directory structure will be created:

% tree cookbooks/smartmontools
├── attributes
├── definitions
├── files
│   └── default
├── libraries
├── metadata.rb
├── providers
├── recipes
│   └── default.rb
├── resources
└── templates
    └── default

10 directories, 3 files

README Driven Development

I’m a big fan of Tom Preston-Werner’s blog post on README driven development. I don’t write the complete README before I start writing code for a new cookbook. I do write it as I go.

In order to write a proper README for a cookbook, and to write the cookbook itself, we’ll need to know a bit more about the software we’re installing. The best way to do that depends on the software, but often it is as simple as merely installing the package on a test system such as a virtual machine and explore its contents.

apt-get install smartmontools
dpkg -L smartmontools

Of note for smartmontools, documentation is in /usr/share/doc/smartmontools and configuration is in /etc. In particular, /etc/smartd.conf and /etc/smartmontools.

For now assume that the cookbook is being written along the way.

List of Resources

One of the things I do when I am writing a new cookbook and exploring the contents of a package is to be mindful of Chef Resources I want to manage in the recipe(s). In the case of smartmontools, at this point I have determined I need a few specific resources.

Install the Package

First, as I’ve installed the package, I clearly need a package. I’m pretty confident that this particular package will not break backwards compatibility, and can be safely upgraded to the latest version if necessary.

package "smartmontools" do
  action :upgrade

Configuration files are templates

The next resources in the recipe are the configuration files. I want to dynamically configure these, so I am going to use templates.

template "/etc/default/smartmontools" do
  source "smartmontools.default.erb"
  owner "root"
  group "root"
  mode 0644
  notifies :reload, "service[smartmontools]"

template "/etc/smartd.conf" do
  source "smartd.conf.erb"
  owner "root"
  group "root"
  mode 0644
  notifies :reload, "service[smartmontools]"

I’m not going to write these from scratch. Instead, I will copy the source files from the installed package on my test system. These will go into templates/default in the cookbook.

% tree templates
└── default
    ├── smartd.conf.erb
    └── smartmontools.default.erb

Templates are dynamically generated using ERB, and they can use Node attributes. I can use the automatically detected attributes, or I can set new attributes for the node in the cookbook.

Attributes Used in the Templates

The attributes go in the attributes/default.rb file in the cookbook. The ones I use are:

default['smartmontools']['start_smartd'] = "yes"
default['smartmontools']['smartd_opts']  = ""
default['smartmontools']['devices']      = []
default['smartmontools']['device_opts']  = "-H -l error -l selftest"

Attributes are definitely something to document in the README.

In templates/default/smartd.conf.erb, I check if there’s a list of devices to monitor, and if so iterate over the list passing in the default options (device_opts). The cookbook doesn’t at this time support per-device options – the same ones are applied to all devices. If devices is empty, then the configuration will use DEVICESCAN.

<% if node['smartmontools']['devices'].length > 0 -%>
<%   node['smartmontools']['devices'].each do |device| -%>
<%=    "/dev/#{device} #{node['smartmontools']['device_opts']}" %>
Posted in:

Joshua Timberman

Joshua Timberman is a Code Cleric at CHEF, where he Cures Technical Debt Wounds for 1d8+5 lines of code, casts Protection from Yaks, and otherwise helps continuously improve internal technical process.

Join Automate for Good hackathon and be eligible to win $60000 in prizes