With the release of Puppet 2.6, a brand new concept was introduced: Puppet faces.
Faces are an API that allow easy creation of new Puppet (sub) commands: whenever we execute Puppet, we specify at least one command, which provides access to the functionalities of its subsystems.
The most common commands are agent
, apply
, master
, and cert
and have existed for a long time but there are a lot more (we can see their full list with puppet help
) and most of them are defined via the faces API.
As you can guess, we can easily add new faces and therefore, new subcommands to the Puppet executable just by placing some files in a module of ours.
The typical synopsis of a face reflects the Puppet command's one:
puppet [FACE] [ACTION] [ARGUMENTS] [OPTIONS]
Where [FACE]
is the Puppet subcommand to be executed, [ACTION]
is the face's action we want to invoke, [ARGUMENTS]
is its arguments, and [OPTIONS]
is general Puppet options.
To create a face, we have to work on two files: lib/puppet/application/<face_name>.rb
and lib/puppet/face/<face_name>.rb
. The code in the application
directory simply adds the subcommand to Puppet extending the Puppet::Application::FaceBase
class; the code in the face
directory manages all its logic and what to do for each action.
An interesting point to consider when writing and using faces is that we have access to the whole Puppet environment, its indirectors and termini, and we can interact with its subsystems via the other faces.
A very neat example of this is the secret_agent
face, which reproduces as a face what the, much older, agent
command does; a quick look at the code in lib/puppet/face/secret_agent.rb
, amended of documentation and marginal code, reveals the basic structure of a face and how other faces can be used:
require 'puppet/face' Puppet::Face.define(:secret_agent, '0.0.1') do action(:synchronize) do default summary "Run secret_agent once." [...] when_invoked do |options| Puppet::Face[:plugin, '0.0.1'].download Puppet::Face[:facts, '0.0.1'].upload Puppet::Face[:catalog, '0.0.1'].download report = Puppet::Face[:catalog, '0.0.1'].apply Puppet::Face[:report, '0.0.1'].submit(report) return report end end end
The Puppet::Face
class exposes various methods. Some of them are used to provide documentation, both for the command line and the help pages: summary
, arguments
, license
, copyright
, author
, notes
, and examples
. For example, the module
face uses these methods to describe what it does in Puppet's core at lib/puppet/face/module.rb
:
require 'puppet/face' require 'puppet/module_tool' require 'puppet/util/colors' Puppet::Face.define(:module, '1.0.0') do extend Puppet::Util::Colors copyright "Puppet Labs", 2012 license "Apache 2 license; see COPYING" summary "Creates, installs and searches for modules on the Puppet Forge." description <<-EOT
This subcommand can find, install
and manage
modules from the Puppet Forge, which is a repository of user-contributed Puppet code. It can also generate empty modules, and prepare locally developed modules for release on the Forge:
EOT display_global_options "environment", "modulepath" end
The action
method is invoked for each action of a face. Here, we pass the action name as a symbol and a block of code, which implements our action using various other methods:
description
, summary
, and returns
option
and arguments
when_invoked
(its return value is the output of the command) and when_rendering
Let's see the implementation of the install
action of the module
face. The following code is in the lib/puppet/face/module/install.rb
file; it's possible to add the code for each action in separate files as in this case, or on the main face file.
We are dealing with a Ruby class that may require other classes:
require 'puppet/forge' require 'puppet/module_tool/install_directory' require 'pathname'
This is followed by the face definition and the code applied for the install
action:
Puppet::Face.define(:module, '1.0.0') do action(:install) do
summary "Install a module from the Puppet Forge or a release archive." description <<-EOT […] EOT returns "Pathname object representing the path to the installed module." examples <<-'EOT' […] EOT
Then, the expected arguments and the available options are defined (copied here is only the block relative to the --target-dir
option, various other are present in the original file and are defined in a similar way):
arguments "<name>" option "--force", "-f" do summary "Force overwrite of existing module, if any." description <<-EOT Force overwrite of existing module, if any. EOT end option "--target-dir DIR", "-i DIR" do summary "The directory into which modules are installed." description <<-EOT […] EOT end
Then, when the install
action is called, the when_invoked
block is executed. Here is where the real work is done, and in this case are mostly called methods from the Puppet::ModuleTool
class and its subclasses:
when_invoked do |name, options| Puppet::ModuleTool.set_option_defaults options Puppet.notice "Preparing to install into #{options[:target_dir]} ..." forge = Puppet::Forge.new("PMT", self.version) install_dir = Puppet::ModuleTool::InstallDirectory.new(Pathname.new(options[:target_dir])) installer = Puppet::ModuleTool::Applications::Installer.new(name, forge, install_dir, options) installer.run end
This action also invokes the when_rendering
block to format the console output:
when_rendering :console do |return_value, name, options| if return_value[:result] == :failure Puppet.err(return_value[:error][:multiline]) exit 1 else tree = Puppet::ModuleTool.build_tree(return_value[:installed_modules], return_value[:install_dir]) return_value[:install_dir] + " " + Puppet::ModuleTool.format_tree(tree) end end end end
As it happens for many faces, most of the code is in the face
directory. The other component of the face, placed in lib/puppet/application/module.rb
, is just an extension to the Puppet::Application::FaceBase
class:
require 'puppet/application/face_base' class Puppet::Application::Module < Puppet::Application::FaceBase end