In my previous post on the Configurer, I only made a passing
reference to pluginsync. However, pluginsync is rather important both because
it’s the mechanism in which Puppet can be extended, but it’s a really
interesting window into how Puppet configures itself. We’ll be looking at
Puppet::Configurer::PluginHandler and Puppet::Configurer::Downloader in this
post.
PluginHandler is a module that contains the basic pluginsync logic, and it’s mixed in to the Configurer. It’s a convenient way to extract that logic out of the Configurer. The really interesting bit is the Downloader, because it shows how Puppet can use the same mechanisms to configure your system to configure itself.
Puppet::Configurer::PluginHandler
The PluginHandler is actually rather small; only implementing the following methods:
download_plugins?download_pluginsload_plugin
To kick off things, in Puppet::Configurer#prepare, the #download_plugins
method is called.
Puppet::Configurer::PluginHandler#download_plugins
The logic behind this method is pretty simple. It calls the #download_plugins?
method to see if pluginsync is enabled. If /etc/puppet/puppet.conf has
pluginsync = true set, or it’s being run with –pluginsync true then it’ll
continue with the sync. If pluginsync is enabled, it instantiates a new
Puppet::Configurer::Downloader object, evaluates it, and for each
plugin synced, calls the #load_plugin method. Straight forward, right? (Oh
yeah, I’m totally skipping over the #evaluate call. We’ll get there shortly.)
Puppet::Configurer::PluginHandler#load_plugin
This method does some basic sanity checking around loading plugins. It makes sure that the plugin is actually loadable, and then tries to load it. If you’ve been developing on a type or provider and made a syntax error, this is where it’ll explode. However, if the actual load fails, the method will capture that failure, throw it away, and continue with the rest of the pluginsync and run.
So how exactly do plugins get transferred from the master to the agent? The answer is, MAGIC! Oh, and the Downloader helps too. But it’s magic.
Puppet::Configurer::Downloader
So this is where things get interesting - Puppet actually creates a new catalog to perform pluginsync. While there’s a lot of talk around Puppet compiling a manifests into a catalog and that’s what’s applied to your system, catalogs can be used for more than just that.
When you think about it, what is pluginsync? Really, it’s just a recursive file definition for your libdir. That is, it looks like this:
file { $libdir:
ensure => directory,
recurse => true,
source => 'puppet:///plugins',
force => true,
purge => true,
backup => false,
noop => false,
}
Heh, so funny story - that’s exactly what’s happening. When you get down to it, the Downloader is just a thin layer around a very simple catalog that contains this one resource. From here on out, things act like you would expect.
So the PluginHandler creates a Downloader, and then calls #evaluate on it.
What’s happening?
Puppet::Configurer::Downloader#initialize
When the PluginHandler creates a new Downloader, it sets up some basic instance variables. They are as follows:
-
name: The name of the catalog. The PluginHandler simply uses ‘plugin’ for
this value. This is the field used when you see
info: Retrieving pluginat the beginning of a run. - path: This is the location of your libdir, so it’s used as the name of the File resource.
-
source: This is the source field for a file resource, and it’ll be used for
the source of the files. Here’s the crazy bit - you can set this as the
‘pluginsource’ option in your puppet.conf, so in theory you could serve
all your plugins via a shared NFS mount. However, it simply defaults to
puppet://$server/plugins -
ignore: This is a whitespace delimited field of files that should be
ignored. This is how Puppet prevents syncing .svn, .git and CVS directories
by default. It can be controlled with the ‘pluginsignore’ setting in
puppet.conf, so you could add
\..*\.swpor.*~to ignore Vim and Emacs swapfiles, respectively.
You’ll notice that all of these are all centered around parameters for a File resource. That’s exactly what’s going on.
Puppet::Configurer::Downloader#file
This method generates a single File resource, and passes the previously mentioned variables to it. It sets up some specific things and does some variable munging, but the core of it is this:
Puppet::Type.type(:file).new(args)
This takes the variables specified in #initialize and generates the File
resource.
Puppet::Configurer::Downloader#catalog
So we have this simple file resource, now we need to add it to a catalog so we can do something with it. This method does just that - but with some exceptions.
ENTERING TANGENT LAND
With a normal catalog, called a “host catalog”, special processing is done. Reports are generated and sent, graphs are made, and the normal operations are done. However, this is just internal Puppet configuration, so you probably don’t care about a report of just this. This catalog is what is known as a “resource catalog” and it’s treated differently inside of Puppet.
This is how Puppet can configure itself, and still retain some semblance of sanity - host catalogs are what you apply to a host, and resource catalogs are what Puppet applies to itself.
EXITING TANGENT LAND
Anywho, this will spit back a resource catalog containing the single file resource created above.
Puppet::Configurer::Downloader#evaluate
When the PluginHandler created a Downloader instance, it immediately called
#evaluate. This is the meat of the Downloader; it creates the catalog and
applies it. The libdir will be recursively synced, and the list of files that
were changed during this process will be recorded and returned.
This recording of the files that were changed is how Puppet only loads files that were changed during pluginsync. You’ll notice that when you run Puppet the first time with pluginsync, a number of files will be loaded following the sync. However, successive runs won’t display this. It only makes sense to reload files that were changed, so this is how it’s implemented.
If you use the dynamic puppet environments from git branches workflow, you may have noticed something odd if you’ve ever tried to run against an environment that doesn’t exist. The way the workflow works is that any environment name works, but only environments that exist will actually do something sane. If you try to run Puppet against an environment that doesn’t exist, you’ll get this:
root@modi:~# puppet agent -t --environment NOTANENVIRONMENT
info: Retrieving plugin
err: /File[/var/lib/puppet/lib]: Could not evaluate: Could not retrieve information from environment NOTANENVIRONMENT source(s) puppet://puppet/plugins
notice: /File[/var/lib/puppet/lib/puppet]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/database_user]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/database_user]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/database_user/mysql.rb]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/database_user/mysql.rb]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/network_config]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/network_config]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/java_ks]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/java_ks]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/modules]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/modules]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/vc_folder]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/vc_folder]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/file_line]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/file_line]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/test]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/test]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/firewall.rb]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/firewall.rb]: Skipping because of failed dependencies
notice: /File[/var/lib/puppet/lib/puppet/provider/sysctl]: Dependency File[/var/lib/puppet/lib] has failures: true
warning: /File[/var/lib/puppet/lib/puppet/provider/sysctl]: Skipping because of failed dependencies
[etc]
From my understanding, this is the effect of this bug. Effectively, the environment and subsequent modulepath is valid but there’s no plugins to be synced, since the directories are unavailable. Since there’s no directory, every single plugin that’s been synced explodes. If you have puppetlabs-stdlib or a number of custom types/providers/functions, this will be a very lengthy explosion. My hat is off to Patrick for resolving this bug and others; he’s done a fantastic job of improving code around Puppet and environments.
So that’s it! At the end of the day, pluginsync is just a File resource using a very simple catalog. These are mechanisms you’ve certainly been using all over the place, but put to use in a really clever way.
Addendum: Puppet 2.7.17 was used as the reference version for this blog.