Features, the forgotten feature of Puppet

When you write enough Puppet code, you will eventually find yourself in need of a Facter fact or Puppet resource type that doesn’t exist in Puppet itself. Then, if you’re like me, you go to the Puppet Forge and see if someone else has written what you need. Oftentimes, you find what you need, add a new module to your Puppetfile or module metadata, and move on with your life.

However, sometimes your search turns up blank and you are confronted with a choice: abandon what you are trying to do, or; write a new custom fact or a new custom type / provider.

When you choose the latter, writing a custom feature or two can save you a lot of heartache. Don’t know what a custom feature is? No worries, this post will walk you through what a custom feature is, when you should use them, and how to write them.

The scenario

A development team just finished writing some new software, AwesomeApp. You, being the resident Puppet aficionado, have been tasked with writing the Puppet code to deploy and configure this software across select machines in your diverse fleet of nodes. Since AwesomeApp is brand new and awesomely different from other software you currently manage, you realize that you will need to write custom Puppet code to deploy and configure the software.

As you continue reading over the requirements from the developers, you realize that you will also need a custom fact and custom type and provider to deploy and configure AwesomeApp. The catch is that the custom fact and the custom type / provider will need AwesomeApp dependencies installed on the node, and not every node will have AwesomeApp installed on it. This means that you will need to confine your fact and type / provider to suitable nodes only so you don’t install those dependencies where they don’t belong.

Confinement and suitability

Custom Facter facts and custom types / providers provide the concepts of confinement and suitability to help you accomplish this task. Confinement means what you think it does: confine this code to only execute if a condition is met. Suitability is also fairly self-descriptive: make sure this node is suitable for this code before this code is executed, often by evaluating any confinement conditions.

Here’s what confinement looks like in the code of a custom Facter fact (it looks nearly identical in custom type / provider code as well):

require ‘puppet’

Facter.add(:cem_inetd) do
  confine kernel: 'Linux'
  ...
  setcode do
    ...
  end
end

Confining a custom Facter fact or custom type / provider involves using the confine function to, typically, only allow resolution of the fact on nodes that can satisfy the parameters you have given it. In the case of the example above, we will only resolve this fact on nodes that have a Facter fact called kernel that resolves to Linux because that is the only value that the fact has deemed suitable. In other words, only Linux nodes will have this fact; Windows nodes will not, because Puppet will evaluate the condition to prove suitability before execution.

Back to our example scenario. You now know that you can leverage confinement and suitability for the AwesomeApp fact and type / provider, but you don’t have a fact to use with confine that states whether or not the dependencies are installed. You start thinking over your options.
You could isolate those nodes in their own environments, but that would introduce more complexity to your node classification.

You could bake the check into the custom fact and custom type / provider code itself, but that would introduce a lot of conditional statements and make your new code more error-prone.

If only Puppet had some sort of feature besides facts that you could use in this situation. A lightweight, easy to use feature that could briefly evaluate suitability on a node and be used with confine.

What would you even call a feature like that, though?

Feature is the feature

Fortunately for us, Puppet does provide this feature in the form of Features. I know, the terminology can be a bit confusing. Features are small snippets of Ruby code that evaluate suitability on a node and expose a function that can be used with confine.

Trust me, it’s much simpler than it sounds.

To prove this, let’s take a look at an actual feature used by the Compliance Enforcement Module for Linux:

# lib/puppet/feature/cem_inetd.rb
require 'puppet/util/feature'

Puppet.features.add(:cem_inetd) do
  inetd = `sh -c 'command -v inetd'`.strip
  xinetd = `sh -c 'command -v xinetd'`.strip
  inetd.empty? && xinetd.empty? ? false : true
end

That’s all there is to a feature. Features are nothing but some Ruby code that declare a feature name and return true, false, or nil. Features can then be used with confine like so:

# lib/facter/cem_inetd.rb
require 'puppet'

Facter.add(:cem_inetd) do
  confine kernel: 'Linux'
  confine { Puppet.features.cem_inetd? }
  setcode do
    ...
  end
end

Easy right? Now for some details.

Where features are found

Features can be added to a Puppet module by adding a ruby file to the path lib/puppet/feature/. The convention is to name your ruby file after the feature name.

Feature return values

Each of the three acceptable feature return values don’t just determine suitability; they also influence how Puppet caches the value of the feature.

  • A feature that returns true has that value cached and the feature code will not be executed again on that node
  • A feature that returns false has that value cached and the feature code will not be executed again on that node
  • A feature that returns nil does not cache that value, and the feature code will execute on that node at every Puppet run.

So, when writing features it is important to think about what exactly you are checking. You should only return false if the check will never be true in the future. You should return nil if the check could possibly be true in the future.

Confine with features

When Puppet detects that a new feature has been added, it automatically creates a new function that is available to use: Puppet.features.<feature name>?. What this function does is return the cached value of the feature, or run the feature code and cache / return the value. When you use this function with confine, it is important to remember to pass the feature function inside of a block (the curly braces) because otherwise you will get an error.

An awesome feature for AwesomeApp

So now that we know all about features, let’s write the features we need for AwesomeApp. Since AwesomeApp depends on xinetd because it listens for network requests to spawn system services (AwesomeApp may not actually be that awesome…), we can reuse the feature from the example code above. Remember, since xinetd is not something we can reasonably expect to exist on a node at some time in the future, we just return false if we can’t find it.

The next feature we need has to validate that two Ruby gems, awesome_gem and gem_awesome, are installed and available to Puppet. Fortunately, features have one more feature that makes this super easy. Here is our new feature code:

# lib/puppet/feature/awesome_ruby_deps.rb
require 'puppet/util/feature'

Puppet.features.add(:awesome_ruby_deps, libs: [‘awesome_gem’, ‘gem_awesome’])

That’s it. The add() function provides a convenience parameter libs that is specifically used for checking that Puppet has the specified Ruby libraries.

TL;DR

Instead of filling custom facts and types / providers full of complex conditional logic to ensure that they only run where they are supposed to and have all they need to run, write features to use with confine instead.

Learn more

28