One of the oldest-established Puppet Forge modules is puppetlabs/stdlib
, the official Puppet standard library. We looked at this briefly earlier in the chapter when we used it as an example of installing a module with r10k
, but let's look more closely now and see what the standard library provides and where you might use it.
Rather than managing some specific software or file format, the standard library aims to provide a set of functions and resources which can be useful in any piece of Puppet code. Consequently, well-written Forge modules use the facilities of the standard library rather than implementing their own utility functions which do the same thing. You should do the same in your own Puppet code—when you need a particular piece of functionality, check the standard library first to see if it solves your problem, rather than implementing it yourself.
As you know, you can install a package using the package
resource, like this (package.pp
):
package { 'cowsay': ensure => installed, }
But what happens if you also install the same package in another class in a different part of your manifest? Puppet will refuse to run, with an error like this:
Error: Evaluation Error: Error while evaluating a Resource Statement, Duplicate declaration: Package[cowsay] is already declared in file /vagrant/examples/package.pp:1; cannot redeclare at /vagrant/examples/package.pp:4 at /vagrant/examples/package.pp:4:1 on node localhost
If both of your classes really require the package, then you have a problem. You could create a class which simply declares the package, and then include that in both classes, but that is a lot of overhead for a single package. Worse, if the duplicate declaration is in a third-party module, it may not be possible, or advisable, to change that code.
What we need is a way to declare a package which will not cause a conflict if that package is also declared somewhere else. The standard library provides this facility in the ensure_packages()
function. Call ensure_packages()
with an array of package names, and they will be installed if they are not already declared elsewhere (package_ensure.pp
):
ensure_packages(['cowsay'])
If you need to pass additional attributes to the package
resource, you can supply them in a hash as the second argument to ensure_packages()
, like this (package_ensure_params.pp
):
ensure_packages(['rake'], { 'provider' => 'gem' })
Why is this better than using the package
resource directly? When you declare the same package
resource in more than one place, Puppet will give an error message and refuse to run. If the package is declared by ensure_packages()
, however, Puppet will run successfully.
Since it provides a safe way to install packages without resource conflicts, you should always use ensure_packages()
instead of the built-in package
resource. It is certainly essential if you're writing modules for public release, but I recommend you use it in all your code. We'll use it to manage packages throughout the rest of this book.
Often when managing configuration with Puppet, we would like to change or add a particular line to a file, without incurring the overhead of managing the whole file with Puppet. Sometimes it may not be possible to manage the whole file in any case, as another Puppet class or another application may be managing it. We could write an exec
resource to modify the file for us, but the standard library provides a resource type for exactly this purpose: file_line
.
Here's an example of using the file_line
resource to add a single line to a system config file (file_line.pp
):
file_line { 'set ulimits': path => '/etc/security/limits.conf', line => 'www-data - nofile 32768', }
If there is a possibility that some other Puppet class or application may need to modify the target file, use file_line
instead of managing the file directly. This ensures that your class won't conflict with any other attempts to control the file.
You can also use file_line
to find and modify an existing line, using the match
attribute (file_line_match.pp
):
file_line { 'set root email alias': path => '/etc/aliases', line => 'root: [email protected]', match => '^root: ', }
The value of match
is a regular expression, and if Puppet finds a line in the file which matches this expression, it will replace it with the value of line
. (If you need to potentially change multiple lines, set the multiple
attribute to true
, or Puppet will complain when more than one line matches the expression.)
You can also use file_line
to delete a line in a file if it is present (file_line_absent.pp
):
file_line { 'remove csh from valid shells': ensure => absent, path => '/etc/shells', match => '^/bin/csh', match_for_absence => true, }
The grep()
function will search an array for a regular expression and return all matching elements (grep.pp
):
$values = ['foo', 'bar', 'baz'] notice(grep($values, 'ba.*')) # Result: ['bar', 'baz']
The member()
and has_key()
functions return true
if a given value is in the specified array or hash, respectively (member_has_key.pp
):
$values = ['foo', 'bar', 'baz'] notice(member($values, 'foo')) # Result: true $valuehash = { 'a' => 1, 'b' => 2, 'c' => 3 } notice(has_key($valuehash, 'b')) # Result: true
The empty()
function returns true
if its argument is an empty string, array, or hash (empty.pp
):
notice(empty('')) # Result: true notice(empty([])) # Result: true notice(empty({})) # Result: true
The join()
function joins together the elements of a supplied array into a string, using a given separator character or string (join.pp
):
$values = ['1', '2', '3'] notice(join($values, '... ')) # Result: '1... 2... 3'
The pick()
function is a neat way to provide a default value when a variable happens to be empty. It takes any number of arguments, and returns the first argument which is not undefined or empty (pick.pp
):
$remote_host = '' notice(pick($remote_host, 'localhost')) # Result: 'localhost'
Sometimes you need to parse structured data in your Puppet code which comes from an outside source. If that data is in YAML format, you can use the loadyaml()
function to read and parse it into a native Puppet data structure (loadyaml.pp
):
$db_config = loadyaml('/vagrant/examples/files/database.yml') notice($db_config['development']['database']) # Result: 'dev_db'
The dirname()
function is very useful if you have a string path to a file or directory and you want to reference its parent directory; for example, to declare it as a Puppet resource (dirname.pp
):
$file = '/var/www/vhosts/mysite' notice(dirname($file)) # Result: '/var/www/vhosts'
When a Puppet manifest doesn't do quite what you expect, troubleshooting the problem can be difficult. Printing out the values of variables and data structures with notice()
can help, as can running puppet apply -d
to see detailed debug output, but if all else fails, you can use the standard library's pry()
method to enter an interactive debugger session (pry.pp
):
pry()
With the pry
gem installed in Puppet's context, you can call pry()
at any point in your code. When you apply the manifest, Puppet will start an interactive Pry shell at the point where the pry()
function is called. You can then run the catalog
command to inspect Puppet's catalog, which contains all the resources currently declared in your manifest:
sudo puppet apply /vagrant/examples/pry_install.pp sudo puppet apply /vagrant/examples/pry.pp ... [1] pry(#<Puppet::Parser::Scope>)> catalog => #<Puppet::Resource::Catalog:0x00000001bbcf78 ... @resource_table={["Stage", "main"]=>Stage[main]{}, ["Class", "Settings"]=>Class[Settings]{}, ["Class", "main"]=>Class[main]{}}, @resources=[["Stage", "main"], ["Class", "Settings"], ["Class", "main"]], ...
Once you've finished inspecting the catalog, type exit
to quit the debugger and continue applying your Puppet manifest.
3.14.69.119