/etc/hosts Management with Chef

We recently ran into a situation where we needed to use Chef Search to modify our /etc/hosts file dynamically on each Chef run. Originally seeming to be a relatively simple task, managing the hosts file with Chef rose some interesting challenges. While there are a few existing community-maintained /etc/hosts management cookbooks out there, none of them suited our needs. We wanted a highly customizable, easily expandable, simple, LWRP that was idempotent.

In this post, I will discuss creating your own LWRP, as well as some of the challenges faced while writing this LWRP. There will also be links to the community LWRP that we've created.

For those that just can't wait!

Here are the links to the hostsfile github repo and hostsfile on the Chef community website.

Working Backwards

Before writing this LWRP, I knew exactly what I wanted the interface to look like. I knew that it needed to follow the patterns of resource built into Chef. I wanted it to be simple, yet configurable, yet still lightweight. This gave me a great opportunity to practice README-drive development. There are a lot of advantages to RDD, especially on smaller projects:

  1. Ensure you have fully thought out your implementation
  2. Chooses your organization structure
  3. Ensures you have a great README
  4. Makes you really focus on usage over implementation

The last option is, in my opinion, the most important. This LWRP would eventually end up on the Chef community site. I asked myself multiple times:

How will people use this?

And that question is king in open source development. If adding a few lines of code to your implementation allows people to more easily understand your "product", then it's worth it. It's always worth it. This cookbook was written for the end-user.

And yes, I actually did write the README first.

Getting Started

With the README done and polished, it was time to start working forward. Luckily, I had a good, solid understanding of the implementation details as a result of the RDD I discussed earlier.

To get started with a custom LWRP, you actually create a cookbook:

$ knife cookbook create hostsfile

... and then delete everything until you're left with:

|_libraries
|_providers
|_resources
|_LICENSE
|_metadata.rb
|_README.md

Spend a few minutes adding all the LICENSE files and replacing "YOUR NAME HERE" where appropriate.

Next, because I already know my implementation, I can actually start by writing the resource file first:

# List of all actions supported by the provider
actions :create, :create_if_missing, :update, :remove

# Make create the default action
default_action :create

# Required attributes
attribute :ip_address, :kind_of => String, :name_attribute => true, :required => true
attribute :hostname, :kind_of => String

# Optional attributes
attribute :aliases, :kind_of => Array
attribute :comment, :kind_of => String

The design is so elegant and makes perfect sense based off our the README.

Now it's time to move onto the more difficult part. Actually writing the provider code (the implementation):

# Creates a new hosts file entry. If an entry already exists, it will be
# overwritten by this one.
action :create do
  ...
end

# Create a new hosts file entry, only if one does not already exist for
# the given IP address. If one exists, this does nothing.
action :create_if_missing do
  ...
end

# Updates the given hosts file entry. Does nothing if the entry does not
# exist.
action :update do
  ...
end

# Removes an entry from the hosts file. Does nothing if the entry does
# not exist.
action :remove do
  ...
end

The full source for the provider entry is available on github. I've suppressed a lot here.

Initial Roadblock

My initial intention was to use the ghost gem for managing the hosts file in Ruby. However, ghost did not support he features I needed, and it did not provide a great interface beyond the command line.

So, I reinvented the wheel.

Reinventing the Wheel

In short, I created my own /etc/hosts management library in Ruby. It's *nix specific, but it works. I spent significantly less time writing my own code than trying to make something like ghost suit my needs.

The library consisted of two files:

It's a lot of file parsing and making things look pretty to be honest.

The Manipulate class works a lot like a database object - you must call save or save! in order to write the changes out.

Take a look at the source on github if your curious, but it's mostly Ruby code. There's actually no Chef magic going on. I could easily have packaged the library into a gem and used that instead...

Usage

The hostsfile LWRP comes equipped with 4 actions:

create

Creates a new hosts file entry. If an entry already exists, it will be overwritten by this one.

hostsfile_entry '1.2.3.4' do
  hostname 'example.com'
  action :create
end

This will create an entry like this:

1.2.3.4          example.com

create_if_missing

Create a new hosts file entry, only if one does not already exist for the given IP address. If one exists, this does nothing.

hostsfile_entry '1.2.3.4' do
  hostname 'example.com'
  action :create_if_missing
end

update

Updates the given hosts file entry. Does nothing if the entry does not exist.

hostsfile_entry '1.2.3.4' do
  hostname 'example.com'
  comment 'Update by Chef'
  action :update
end

This will create an entry like this:

1.2.3.4           example # Updated by Chef

remove

Removes an entry from the hosts file. Does nothing if the entry does not exist.

hostsfile_entry '1.2.3.4' do
  action :remove
end

This will remove the entry for 1.2.3.4.

I want it now!

Install it

$ knife cookbook site install hostsfile

Include it

# recipes/my_recipe.rb
include_recipe 'hostsfile'

You're done!

by Seth Vargo