Thursday, April 10, 2008

Beware, Puppet power users!

Puppet is a tool to automate system administration and to save you from boring and repetitive tasks. The various services, packages and configuration files of the various hosts are organized into nodes. It's pretty interesting because nodes can be arranged in a tree, so that the configuration that is common between several machines is written only once, and inherited by the child nodes. Also, definitions let you write modular pieces of system configuration, with parameters, and default values.

Puppet is written in Ruby, so it may sound fantastic news for power users that wish to extend Puppet capabilities. But there are some limitations that even a novice Puppet user can already face:

  1. Configuration is not written in Ruby, so you have to stick with the plain Puppet vocabulary. That's actually good to have a well-defined format that models the basic concepts like package, service, file, etc, and that's a guarantee to survive a Ruby upgrade or even a complete Puppet rewrite. But Puppet has reinvented variables for example, and without any object-oriented design. So you're stuck with some kind of artificial global variables following poorly-documented rules. So I would actually warn against using Puppet variables.

  2. Classes, as it's called in the Puppet semantic, are not classes like you would expect in the object-oriented sense. Classes in Puppet are just encapsulating other configuration bits. Using those classes simply consists in including their definition. Do not expect any sophisticated inheritance or any other OOP feature here. I would recommend to only use definitions, that are reusable wrappers with support for parameters and default values.

  3. Puppet templates (actually ERB templates) can reference Puppet variables defined in the configuration, but this is a hack, so don't expect sophisticated constructs here. Template writers don't have access to any high-level Puppet-oriented API. You can use plain Ruby constructs however, like string manipulation, math functions, etc. And the error reporting is so weak, see for yourself:
    err: Could not retrieve catalog: Uncaught exception compile error
    (erb):73: syntax error, unexpected kEND, expecting $end
    end ; _erbout.concat "\n"
    ^ in method puppetmaster.getconfig

    You have to restart puppetmasterd in debug mode and use erb -x -T '-' mytemplate.erb to find out what goes wrong!

  4. Documentation is written in a wiki. It's based on user contribution, so don't expect to receive more than you can give yourself. I suggest reading Pulling Strings with Puppet, although I haven't read it myself, but I hope it can offer some more in-depth understanding of Puppet. (On a side note, the Wiki is using a Trac plugin providing reStructuredText syntax, so make sure to preview your changes, it's not the Trac syntax).

Now let's look at the promise of extensibility.

Writing a custom function

That looks pretty seducing at a first glance, but it just doesn't work:

  1. All so-called functions are run on the server. It's a bit misleading, and I think it is a wrong design. Not being able to write a custom function that will be run on the client side does not make sense. When I'm writing bits of configuration, I'm communicating to Puppet the details that make sense on the local machine, I'm never referencing files that are local to the server! At least the Puppet syntax for a function call should reflect the fact that functions are actually parser functions, not general-purpose functions.

  2. Again, there is no OO design. For example, creating a function is achieved through calling Puppet::Parser::Functions::newfunction(:myfunction)

  3. As you can see in newfunction example above, Puppet internals make extensive use of Ruby symbols, which in my opinion reflects the absence of a real API, and the lack of a real internal design.

Also, note that the Ruby code has to be deployed both on the server and on the client. The fileserver along with the pluginsync mechanism allows to distribute the code automatically on all clients, although I haven't tested this myself.

Writing a custom type

So, you want to write a custom thing that will be executed on the client? There is no choice, you need to write a custom type. But the problem here is that I won't be able to talk a lot about it, because after spending 6 hours trying to hack a custom type with a lot of copy paste, I'm not satisfied with the result, and although I'm both new to Ruby and Puppet-internals, I don't think anyone can actually write any useful custom thing with Puppet. Puppet really needs a true API, be it in Ruby or not. The fact that the manifests are written in an independent syntax offers the possibility to remain compatible in the eventuality of a complete rewrite of the Puppet API.

The word of the end

If you need to extend the core capabilities of Puppet, be warned! There is no reliable and documented way to achieve it. Puppet is a great tool to automate system deployment and maintenance, but still lacks a true API with clearly documented extension points. Stick with simple documented Puppet constructs, use the exec type with hardcoded paths if there is no other way, or better: prepare your data on the server-side beforehand outside the control of Puppet if you need something more elaborate. For example, to concatenate Apache htpasswd files, I ended up writing a two-line shell script in the post-update hook of my Git repository. Much simpler than trying to concatenate files with Puppet! Extending Puppet is far from being a trivial task, even for an expert programmer!


Unknown said...

Don't you think the tone of this post is a bit incendiary? Sure, Puppet's got its problems, like any other tool does, but lots of people are using it today to huge advantage.

We're working on making it better, and we're getting really significant contribution from all around the community.

Luke Kanies (author of Puppet)

Jean-Baptiste Quenot said...

Dear Luke, first let me tell you that your work (and the work of other contributors) on Puppet is very valuable.

This blog post is not a criticism about Puppet in general, but a harsh yet constructive analysis about the ability of Puppet to extend its core capabilities.

I'm a contributor to Open Source in general, and it's true that I was really disappointed by the absence of a well-documented and clear Puppet development API, so much that I finally gave up trying to write any extension.

Before writing this post, I also participated in a number of threads on the mailing-lists, and was not satisfied with the outcome.

Be assured that I spent a lot of time trying to figure this out, I'm talking of days here, but I'm also looking forward to find solutions to all of this in the long term and contribute code, provided that the core design of Puppet offers true extension points.