Ruby Talk Proposals for FOSCON
This is a post from Luke's old blog; it is saved here statically for historical purposes, as of October 2008
I got an email from Thomas Lockney about FOSCON, asking if I were interested in talking about the crazy stuff I'm doing with Ruby in Puppet. Here's what I sent him:
Generating classes rather than declaring them
...which I do throughout Puppet. For instance, here is a recent set of classes I created in Puppet:
module Puppet
newtype(:maillist) do
@doc = "Manage email lists. This resource type currently can only create
and remove lists, it cannot reconfigure them."
newparam(:name, :namevar => true) do
desc "The name of the email list."
end
newparam(:description) do
desc "The description of the mailing list."
end
...
Both newtype and newparam create new classes. I've got enough of this that I've actually created a module to do this for me.
I only do this when I'm storing references to the class or module by name, and this classgen module sticks the generated class into a hash or array as necessary, sets up constants (which I don't use but Ruby does), sets attributes, calls initializations hooks, and much more.
I could fake a lot of this using the 'inherited' class method is called on the parent class, but the class when passed to that method is uninitialized (no associated code has yet been evaluated), it does not work well for subclasses of subclasses, and it doesn't make it easy to reload classes in memory.
Using Racc
...to write a real domain specific language. Everyone asks why I do this, instead of just using Ruby, but this talk would focus more on the how and what than the why. As far as I know, I've got the only two complete and open-source Racc parsers available in the wild. Puppet's parser uses abstract syntax trees and thus is a far more complicated and thus more enlightening parser, while Naginator's parser is pleasantly simple to understand and is a good example. In both cases, I've created my own lexer, since Ruby seems to be without a good lexer generator.
Puppet's configuration system
I've created a stand-alone configuration class that handles all of my Puppet run-time configuration. You define configuration settings with a section and a default:
self.setdefaults(:main,
:trace => [false, "Whether to print stack traces on some errors"],
:autoflush => [false, "Whether log files should always flush to disk."],
:syslogfacility => ["daemon", "What syslog facility to use when logging to
syslog. Syslog has a fixed list of valid facilities, and you must
choose one of those; you cannot just make one up."],
:statedir => { :default => "$vardir/state",
:mode => 01777,
:desc => "The directory where Puppet state is stored. Generally,
this directory can be removed without causing harm (although it
might result in spurious service restarts)."
},
....
See Puppet's configuration for many more examples.
I keep a global configuration object at Puppet.config, set all of my items there, and then I can access them or set them throughout my system with ease.
There are some really great things about this system:
- It's autodocumenting, because every configuration item has a documentation string associated with it directly (see the configuration reference for the current docs).
- It's got methods for setting options within getopt, so you can trivially make any valid config option a valid getopt option, and the integration correctly handles booleans (e.g., --no-noop disables and --noop enables).
- Any configuration parameter can reference another parameter, which is especially useful for directory paths. Just use a '$' before the parameter name, and the config class will expand the item when it is asked for (not when it's set, so it's using lazy evaluation).
The best thing about the class, though, is that it relies on Puppet. Any configuration item that manages files or directories can be converted into a Puppet resource and then evaluated, thus creating the directories if necessary and setting owner, group, and/or mode as necessary. Alternatively, you can generate Puppet code that you can then run manually if you prefer. You can even have Puppet create any necessary users and groups. You can use specific configuration sections (as defined during the configuration specification process), and only the files and directories from those sections will be used, so you can have hundreds of settings but only actually manage the settings you're using.
Provider localization
Puppet's portable resource types, like packages and users, are generally backed by less portable providers, like useradd, dpkg, apt, and about 22 other package providers. These providers vary in suitability for a given platform based on lots of different criteria, and Puppet still has to choose a default provider out of all of those that are suitable.
In addition, most providers are largely just wrappers around external binaries, which they call then parse their output. Because this is done so often throughout the providers, it's important that these calls to external binaries be easy to perform, along with it being easy to manage where the binaries are.
I've created a few simple methods that handle all of this functionality for me. I've got a confine class method that can confine a provider to only systems matching the specified critiria, which can include the presence of a file (usually a binary), comparing the output from Facter so you can confine providers to only Darwin systems, for instance, or arbitrary booleans, even returned from a block.
I went a bit further in handling required binaries by providing a commands method that allows you to specify required commands, either qualified or not, and the provider will then base its suitability on the presence of those commands, store the path for the command so the provider does not need to keep using a full path, and then define a method named after each command that will call out to the command, protecting the call and failing appropriately if the command is missing.
So, I can have simple provider definitions like this:
Puppet::Type.type(:package).provide :apt, :parent => :dpkg, :source => :dpkg do
# Provide sorting functionality
include Puppet::Util::Package
desc "Package management via ``apt-get``."
has_feature :versionable
commands :aptget => "/usr/bin/apt-get"
commands :aptcache => "/usr/bin/apt-cache"
commands :preseed => "/usr/bin/debconf-set-selections"
defaultfor :operatingsystem => :debian
...
Here I've specified three required commands, and now, for instance, I can just call aptget :install, :ruby (since the command accepts symbols as well as strings) and Puppet will call the right binary with those arguments and return the output, or throw an exception if the command fails.
You can see that I've also got a defaultfor method, which I can use to define whether a given provider is the default for some set of systems. We usually use operating system names and releases, but it can be anything that Facter knows about.
Note that this is another example of class generation -- each provider is a generated class, and you can specify as a parent class either a normal Ruby constant or the name of another provider to use as the parent class. Note that I'm also passing other options to the provider, which will get set before the associated code is evaluated.
There are plenty of other things that I think are pretty cool about Puppet, like the new provider features (search for features), which provide automatic documentation and validation, but these four are probably the coolest and most surprising things I do with Ruby in Puppet. If you need more talks, just let me know. :)