This article is a follow-up of those previous two articles of this series on Puppet Internals:
Today we’ll cover the The Indirector. I believe that at the end of this post, you’ll know exactly what is the indirector and how it works.
The puppet source code needs to deal with lots of different abstractions to do its job. Among those abstraction you’ll find:
Each one those abstractions can be found in the Puppet source code under the form of a model class. For instance when Puppet needs to deal with the current node, it in fact deals with an instance of the node model class. This class is called
Each model can exist physically under different forms. For instance Facts can come from Facter or a YAML file, or Nodes can come from an ENC, LDAP, site.pp and so on. This is what we call a Terminus.
The Indirector allows the Puppet programmer to deal with model instances without having to manage herself the gory details of where this model instance is coming/going.
For instance, the code is the same for the client call site to find a node when it comes from an ENC or LDAP, because it’s irrelevant to the client code.
So you might be wondering what the Indirector allows to do with our models. Basically the Indirector implements a basic CRUD (Create, Retrieve, Update, Delete) system. In fact it implements 4 verbs (that maps to the CRUD and REST verb sets):
- Find: allows to retrieve a specific instance, given through the
- Search: allows to retrieve some instances with a search term
- Destroy: remove a given instance
- Save: stores a given instance
You’ll see a little bit later how it is wired, but those verbs exist as class and/or instance methods in the models class.
So back to our
Puppet::Node example, we can say this:
1 2 3 4 5 6 7 8 9 10 11 12
And this works for all the managed models, I could have done the exact same code with certificate instead of nodes.
For the Latin illiterate out-there, terminii is the latin plural for terminus.
So a terminus is a concrete class that knows how to deal with a specific model type. A terminus exists only for a given model. For instance the catalog indirection can use the Compiler or the YAML terminus among half-dozen of available terminus.
The terminus is a class that should inherit somewhere in the class hierarchy from
Puppet::Indirector::Terminus. This last sentence might be obscure but if your terminus for a given model directly inherits from
Puppet::Indirector::Terminus, it is considered as an abstract terminus and won’t work.
1 2 3 4 5 6 7 8 9 10 11 12 13
request parameter used above is an instance of
Puppet::Indirector::Request. This request object contains a handful property that might be of interest when implementing a terminus. The first one is the
key method which returns the name of the instance we want to manipulate. The other is
instance which is available only when saving is a concrete instance of the model.
Implementing a terminus
To implement a new terminus of a given model, you need to add a ruby file of the terminus name in the
For instance if we want to implement a new source of puppet nodes like storing node classes in DNS TXT resource records, we’d create a
puppet/node/dns.rb file whose find method would ask for TXT RR using
Puppet already defines some common behavior like yaml based files, rest based, code based or executable based. A new terminus can inherit from one of those abstract terminus to inherit from its behavior.
I contributed (but hasn’t been merged yet) and OCSP system for Puppet. This one defines a new indirection:
ocsp. This indirection contains two terminus:
The real concrete one that inherits from
Puppet::Indirector::Code, it in fact delegates the OCSP request verification to the OCSP layer:
1 2 3 4 5 6 7 8 9 10 11
It also has a REST terminus. This allows for a given implementation to talk to a remote puppet process (usually a puppetmaster) using the indirector without modifying client or server code:
1 2 3 4 5 6 7 8 9
As you can see we can do a REST client without implementing any network stuff!
To tell Puppet that a given model class can be indirected it’s just a matter or adding a little bit of Ruby metaprogramming.
To keep my OCSP system example, the OCSP request model class is declared like this:
1 2 3 4 5 6 7 8 9 10
Basically we’re saying the our model
Puppet::SSL::Ocsp::Request declares an indirection
ocsp, whose default terminus class is
ca. That means, if we straightly try to call
puppet/indirection/ocsp/ca.rb file will be used.
There’s something I didn’t talk about. You might ask yourself how Puppet knows which terminus it should use when we call one of the indirector verb. As seen above, if nothing is done to configure it, it will default to the terminus given on the
But it is configurable. The
Puppet::Indirector module defines the
terminus_class= method. This methods when called can be used to change the active terminus.
For instance in the puppet agent, the catalog indirection has a REST terminus, but in the master the same indirection uses the compiler:
1 2 3 4 5
In fact the code is a little bit more complicated than this for the catalog but in the end it’s equivalent.
There’s also the possibility for a puppet application to specify a routing table between indirection and terminus to simplify the wiring.
More than one type of terminii
There’s something I left aside earlier. There are in fact two types of terminii per indirection:
- regular terminus as we saw earlier
- cache terminus
For every model class we can define the regular indirection terminus and an optional cache terminus.
Then when finding for an instance the cache terminus will first be asked for. If not found in the cache (or asked to not get from the cache) the regular terminus will be used. Afterward the instance will be
saved in the cache terminus.
This cache is exploited in lots of place in the Puppet code base.
Among those, the
catalog cache terminus is set to
:yaml on the agent. The effect is that when the agent retrieves the catalog from the master through the
:rest regular terminus, it is locally saved by the yaml terminus. This way if the next agent run fails when retrieving the catalog through REST, it will used the previous one locally cached during the previous run.
Most of the certificate stuff is handled along the line of the catalog, with local caching with a file terminus.
REST Terminus in details
There is a direct translation between the REST verbs and the indirection verbs. Thus the
- transforms the indirection and key to an URI:
- does an HTTP GET|PUT|DELETE|POST depending on the indirection verb
On the server side, the Puppet network layer does the reverse, calling the right indirection methods based on the URI and the REST verb.
There’s also the possibility to sends parameters to the indirection and with REST, those are transformed into URL request parameters.
The indirection name used in the URI is pluralized by adding a trailing ‘s’ to the indirection name when doing a search, to be more REST. For example:
GET /production/certificate/test.daysofwonder.comis find
GET /production/certificates/unusedis a search
When indirecting a model class, Puppet mixes-in the
Puppet::Network::FormatHandler module. This module allows to
convert an instance from and to a serialized format. The most used one in Puppet is called
pson, which in fact is json in disguised name.
During a REST transaction, the instance can be serialized and deserialized using this format. Each model can define its preferred serialization format (for instance catalog use pson, but certificates prefer raw encoding).
On the HTTP level, we correctly add the various encoding headers reflecting the serialization used.
You will find a comprehensive list of all REST endpoint in puppet here
Puppet 2.7 indirection
The syntax I used in my samples are derived from the 2.6 puppet source. In Puppet 2.7, the dev team introduced (and are now contemplating removing) an
indirection property in the model class which implements the indirector verbs (instead of being implemented directly in the model class).
This translates to:
1 2 3 4 5
Gory details anyone?
OK, so how it works?
Let’s focus on
- Ruby loads the
- When mixing in
Puppet::Indirectorwe created a bunch of find/destroy… methods in the current model class
- Ruby execute the
indirectscall from the
- This one creates a
Puppet::Indirector::Indirectionstored locally in the
indirectionclass instance variable
- This also registers the given indirection in a global indirection list
- This also register the given default terminus class. The terminus are loaded with a
Puppet::Util::Autoloaderthrough a set of
- This one creates a
- When this terminus class is loaded, since it somewhat inherits from
Puppet::Indirector:Terminus#inheritedruby callback is executed. This one after doing a bunch of safety checks register the terminus class as a valid terminus for the loaded indirection.
- We’re now ready to really call
findis one of the method that we got when we mixed-in
findfirst create a
Puppet::Indirector::Request, with the given key.
- It then checks the terminus cache if one has been defined. If the cache terminus finds an instance, this one is returned
finddelegates to the registered terminus, by calling
- If there’s a result, this one is cached in the cache terminus
- and the result is returned
Pretty simple, isn’t it? And that’s about the same mechanism for the three other verbs.
It is to be noted that the terminus are loaded with the puppet autoloader. That means it should be possible to add more indirection and/or terminus as long as paths are respected and they are in the
I don’t think though that those paths are pluginsync’ed.
I know that the indirector can be intimidating at first, but even without completely understanding the internals, it is quite easy to add a new terminus for a given indirection.
On the same subject, I highly recommends this presentation about Extending Puppet by Richard Crowley. This presentation also covers the indirector.
This article will certainly close the Puppet Extension Points series. The last remaining extension type (Faces) have already been covered thoroughly on the Puppetlabs Docs site.
The next article will I think cover the full picture of a full puppet agent/master run.