Chapter 13. Designing a Custom Module

In this chapter, we will review the process of creating a custom module. We will cover each step from the beginning, including:

  1. Naming a module properly
  2. Generating an empty module skeleton
  3. Writing the base manifest
  4. Identifying files to be copied to the node
  5. Creating templates to customize files on the node
  6. Testing that the module works as expected

Let’s get started.

Choosing a Module Name

A module can have any name that begins with a letter and contains only lowercase letters, numbers, underscores. A hyphen is not allowed within a module name. Here are some valid and invalid module names:

mymodule Valid
3files Invalid
busy_people Valid
busy-people Invalid

It is important to choose your module name carefully to avoid conflicts with other modules. As each module creates its own namespace, only one module may exist within a Puppet catalog with a given name at the same time. For most intents and purposes, this means that you can never use two modules with the same name.

When naming modules, I try to avoid naming the module anything that conflicts with a module name available in the Puppet Forge. I never know when another team might utilize that module for its project, or when a module I download might use the Puppet Forge module as a dependency.

Note
It’s possible to manage conflicting module names by utilizing them within separate environments. We’ll cover how to do this in Chapter 29.

Avoiding Reserved Names

Some namespaces are internal to and used by Puppet itself. For this reason, you can never create a module with these names:

main This class contains any resources not contained by any other class.
facts This namespace contains facts provided by the node’s Puppet agent.
server_facts This namespace contains server-side facts supplied by the Puppet server.
settings This namespace contains the Puppet configuration settings.
trusted This namespace contains facts taken from the client’s certificate, as signed by the Puppet certificate authority.

Generating a Module Skeleton

Puppet will generate an empty skeleton for you with the puppet module generate command. As mentioned at the start of the book, your first module will manage the Puppet agent itself. Let’s create that now. Use your own name or organization name instead of myorg.

When you generate a module, it will ask you a number of questions. Default values used if you just press Enter are included in square brackets after each question:

$ puppet module generate myorg-puppet
We need to create a metadata.json file for this module.  Please answer the
following questions; if the question is not applicable to this module, feel free
to leave it blank.

Puppet uses Semantic Versioning (semver.org) to version modules.
What version is this module?  [0.1.0]
-->

After you have answered all of the questions, it will generate the module in your current directory:

Notice: Generating module at /home/vagrant/puppet...
Notice: Populating templates...
Finished; module generated in puppet.
puppet/Gemfile
puppet/Rakefile
puppet/examples
puppet/examples/init.pp
puppet/manifests
puppet/manifests/init.pp
puppet/spec
puppet/spec/classes
puppet/spec/classes/init_spec.rb
puppet/spec/spec_helper.rb
puppet/tests
puppet/tests/init.pp
puppet/README.md
puppet/metadata.json

If you prefer to skip the questions and edit the file yourself, add --skip-interview to the command line.

Modifying the Default Skeleton

After you’ve been creating modules for a while, you may want to tune the default module skeleton to add things you want in your modules. You do so by placing your revised skeleton in the ~/.puppetlabs/opt/puppet/cache/puppet-module/skeleton directory.

Warning

Previous versions of Puppet used the ~/.puppet/var/puppet-module/skeleton directory. You’ll need to copy any existing skeleton to the new directory:

$ mkdir -p ~/.puppetlabs/opt/puppet/cache/puppet-module/
$ cd ~/.puppetlabs/opt/puppet/cache/puppet-module/
$ cp -r ~/.puppet/var/puppet-module/skeleton ./

You can install multiple skeletons in a directory of your choice, and select one for the module you will be building like so:

$ puppet module --module_skeleton_dir=~/skels/rails-app generate myorg-railsapp

You can also find enhanced module skeletons that others have created. There are skeletons that include better test suites, and skeletons that include common examples for different application environments. You can find Puppet module skeletons by searching for “puppet skeleton” on GitHub.

Understanding Module Structure

Let’s review the files and directories created in your new module. All of the following files are fixed, unchangeable paths built into Puppet’s expectations and utilization of modules:

manifests/ Directory where Puppet language manifests (classes) are read.
files/ Directory to place files served intact by your module.
examples/ Directory to place functional examples used for testing the module.
templates/ Directory to place templates parsed to create custom files.
lib/ Directory for optional module plugins, such as Ruby facts, functions, etc.
spec/ Directory containing unit tests to validate the manifests.
tests/ Directory containing system tests to validate the manifests.
facts.d/ Directory containing non-Ruby external facts to be distributed.
metadata.json File documenting the module version and dependencies.

Installing the Module

To test the module, we’ll have to place it somewhere that Puppet can find it. While developing a new module, it is easiest to simply move the directory into a test environment’s $modulepath:

[vagrant@client ~]$ cd /etc/puppetlabs/code/environments/test/modules
[vagrant@client modules]$ mv ~/puppet ./

Now that the module is in place, add it to classes for the host you are going to test it on:

[vagrant@client ~]$ $EDITOR /etc/puppetlabs/code/hieradata/hostname/client.yaml

Add this file to the classes loaded by your test node:

---
classes:
  - puppet

When you apply the test environment to the client node, this class will be included. This allows for testing the module in isolation.

Creating a Class Manifest

Let’s start within the manifests/ directory of your module. In this directory, you will find a single file named init.pp. This file must exist within every Puppet module, and it must contain the definition for the base class. The base class is a class with the same name as the module. Let’s take a look at that now:

[vagrant@client modules]$ cd puppet/manifests
[vagrant@client manifests]$ cat init.pp
class puppet {

}

Right now this class has no definition. That is an acceptable situation—the class must be defined, but it doesn’t have to do anything. We’ll flesh it out in the very next section, after we discuss the difference between a class and a manifest.

You will observe that the file contains a documentation template above this class definition. Ignore this for now. We’ll cover creating and updating module documentation in Chapter 16.

What Is a Class?

In Part I of the book, you created and applied Puppet manifests. Here’s a brief summary to refresh your memory:

  • Manifests use Puppet configuration language to define configuration policy
  • Manifests contain resources that describe how their target type should be configured
  • Manifests execute immediately with the puppet apply command

The good news for you is that a class is a manifest with special properties. It uses Puppet configuration language to declare resources exactly the same as in a manifest. This means that if you know how to write a manifest, then you know how to write a class.

Before we get started, let’s quickly review the special properties that make a class different from a manifest. A class is different in the following ways:

  • A class is a manifest that can be called by name.
  • A class has a namespace or variable scope of the same name.
  • A class is not used until called by name.
  • A class may include or be included by other modules.
  • A class may be passed parameters when called.

Declaring Class Resources

As we mentioned previously, class manifests are almost exactly like the manifests you used with puppet apply. I am sure that you remember (if not, flip back to “Referring to Resources”) the resources declared to install a package and start a service. Go ahead and fill in the class manifest with those resources right now.

The manifest should look something like this:

class puppet {
  # Install the Puppet agent
  package { 'puppet-agent':
    ensure => 'latest',
    notify => Service['puppet'],
  }

  # Manage the Puppet service
  service { 'puppet':
    ensure    => 'running',
    enable    => true,
    subscribe => Package['puppet-agent'],
  }
}

This is very similar to the package and service manifest built out earlier in the book. As per best practice, we have defined dependencies to ensure that the service is restarted if the package is updated.

Let’s apply the test environment to the node now for our first test of the module:

[vagrant@client modules]$ puppet apply --environment test ../manifests/
Notice: Compiled catalog for client.example.com in environment test
Notice: Applied catalog in 0.02 seconds

Accepting Input

Let’s define parameters your Puppet module will accept. Adjust the init.pp file to look something like this:

class puppet(
  # input parameters and default values for the class
  $version = 'latest',
  $status  = 'running',
  $enabled,              # required parameter
) {

  # echo the input provided
  notice("Install the $version version of Puppet,
    ensure it's $status, and set boot time start $enabled.")
}

Here we have declared three input parameters. Two of them have default values, which will only be used if input is not provided when the class is called. The third value enabled is required, and will generate an error if a value is not provided.

Puppet 4.3 introduced a tiered, pluggable approach to data access called Puppet Lookup. Classes can now receive parameter input from three possible places. If the data is not available in the first place, it looks in the next according to the following flow:

Parameter values can be explicitly passed when the class is declared
You can define parameters to pass when declaring classes in a manifest, or the output of an external node classifier (ENC). Using resource definitions to declare classes in manifests was the only available method prior to data lookups from Hiera, and dominates the older documentation available on the Internet.
Parameter values will be looked up in the data providers (Hiera, environment, module)
This is the most practical way to assign values. The key will be searched for in the global data provider (Hiera v3) followed by the environment and module data providers (if defined). The results from the first data source that contains the key will be returned.
Default values can be supplied in the class definition
The Puppet Style Guide requires that every class should include default values that provide the most common use case.

Let’s go ahead and test our new module to ensure the default values work as expected:

$ puppet apply --environment test ../manifests/
Error: Evaluation Error: Error while evaluating a Function Call,
  Must pass enabled to Class[Puppet]
  at /etc/puppetlabs/code/environments/test/manifests/site.pp:2:1

Whoops! That’s right, we made the enabled parameter required. However, we have not provided a value for this key in any data provider (Hiera is the only one enabled by default). This is one of the reasons that the Puppet Style Guide recommends supplying defaults for all parameters.

Best Practice

Provide every parameter with a default value that will implement the most common use case.

Adjust the module to provide a default value and try again:

class puppet(
  $version = 'latest',
  $status  = 'running',
  $enabled = true,
$ puppet apply --environment test ../manifests/
Notice: Scope(Class[Puppet]): Install the latest version of Puppet,
    ensure it's running, and set boot time start true.
Notice: Compiled catalog for client.example.com in environment test
Notice: Applied catalog in 0.02 seconds

As you can see, the module will apply properly without any parameters provided.

Sharing Files

You’re probably thinking that this manifest isn’t sufficient. One can’t generally install and run software without configuring it. I agree. Let’s add a configuration file.

The first step is to create a directory in which to store files. A module’s files must reside in a files/ directory within the module. You can add subdirectories to organize files:

$ cd /etc/puppetlabs/code/environments/test/modules/puppet
$ mkdir files

Let’s copy our existing puppet.conf configuration file to that directory to test out the concept:

$ cp /etc/puppetlabs/puppet/puppet.conf files/
$ $EDITOR files/puppet.conf

Let’s make a small change to Puppet’s configuration. Add the following line to make the logging a bit more verbose:

[main]
    log_level = notice

Now we’ll add a file resource to the class manifest. Instead of using the content attribute, we’ll use a source attribute. Files specified by this attribute are copied intact to the node. Add the resource to manifests/init.pp:

file { '/etc/puppetlabs/puppet/puppet.conf':
  ensure => 'file',
  owner  => 'root',
  group  => 'wheel',
  mode   => '0644',
  source => 'puppet:///modules/puppet/puppet.conf',
}

There are three valid URIs for use as file sources:

  • puppet:///modules/module_name/filename
  • file:///path/to/file/on/local/system
  • http: or https://web-server/filename (new in Puppet 4.4)
Note

Downloading files over http/https only works well if you add this attribute to the file resource, as remote web servers don’t provide file checksums:

checksum => 'mtime'

The best practice is to store all files used by a module within the module, and use the preceding puppet:/// URI pattern. Leaving the server field in the URI blank indicates to download the file from the Puppet server the agent is currently talking to. This same URI also works seamlessly with puppet apply to get the module files from modulepath on the local system.

The path /modules/puppet is a special path that maps to the files/ directory in the puppet module.

Best Practice

Avoid specifying an explicit Puppet server in the URI source for a file, as the module will fail if any node doesn’t have direct access to that server. Synchronize the dependent files to all Puppet servers that serve the module.

You can specify an array of file sources. Puppet will go through the array of sources and use the first file it finds.

It is possible to source files that are not within a module from the Puppet server. However, this practice is deprecated and not recommended for many good reasons, so I am not going to cover it.

Best Practice

Place files in the same module as the manifests that use the files.

Testing File Synchronization

Let’s stop and test the file copy feature before we proceed. You should see something like this:

[vagrant@client test]$ sudo puppet apply --environment test manifests/
Notice: Compiled catalog for client.example.com in environment test
Notice: /Stage[main]/Puppet/File[/etc/puppetlabs/puppet/puppet.conf]/content:
  content changed '{md5}2c15ae72acdbd8878a6550275bc15fef'
               to '{md5}b0547c4cf4cc5a5cb7aee439765f82a4'
Notice: /Stage[main]/Puppet/File[/etc/puppetlabs/puppet/puppet.conf]/owner:
  owner changed 'vagrant' to 'root'
Notice: /Stage[main]/Puppet/File[/etc/puppetlabs/puppet/puppet.conf]/group:
  group changed 'vagrant' to 'wheel'
Notice: Applied catalog in 0.37 seconds

View the contents of the file, and you’ll find that it has copied the file exactly.

One of the things changed by our file resource was the permissions of the file. You may want to change them back for convenience while learning:

$ chown vagrant:vagrant /etc/puppetlabs/puppet/puppet.conf
$ chmod 0664 /etc/puppetlabs/puppet/puppet.conf

Alternatively, you could change the owner and group attributes and run Puppet again.

You can use puppet filebucket to restore the file. First, query the filename to get the most recent hash, and then supply the hash to the restore command:

$ sudo puppet filebucket -l list | grep puppet.conf
c15ae72acdbd8878a6550275bc 2015-09-07 03:58:51 /etc/puppetlabs/puppet/puppet.conf
8c1fd25aed09e7b2c8cfc3b0eb 2015-09-07 08:36:39 /etc/puppetlabs/puppet/puppet.conf
bc1a4e205b7e2ec921fee7380d 2015-09-07 08:50:34 /etc/puppetlabs/puppet/puppet.conf

$ sudo puppet filebucket restore 
    /etc/puppetlabs/puppet/puppet.conf c15ae72acdbd8878a6550275bc

Synchronizing Directories

The file resource has optional recurse and purge attributes that make it possible to synchronize a directory of files:

file { '/tmp/sync/':
  ensure  => directory,
  owner   => 'root',
  group   => 'wheel',
  mode    => '0444',
  recurse => true,   # go into subdirectories
  replace => true,   # replace any files that already exist
  purge   => false,  # don't remove files that we don't have
  links   => follow, # follow symbolic links and modify the link target
  force   => false,
  source  => 'puppet:///modules/puppet/sync/',
}

As you can see, there are many parameters for controlling how files are installed, and under what situations they will replace other files. You can find detailed explanations for these attributes at “Puppet 4 Type Reference: file” on the Puppet docs site.

Parsing Templates

I imagine you’re saying to yourself, “Not every system will get the same configuration.” Good point! Let’s build a customized template for this configuration file.

Create a directory in which to store templates. Like files, templates have their own directory in the module structure:

$ cd /etc/puppetlabs/code/environments/test/modules/puppet
$ mkdir templates

The template we are going to build will utilize four variables. Let’s build a manifest that supplies those variables. First, let’s modify the module input to set default values for each of these variables:

class puppet(
  $version         = 'latest',
  $status          = 'running',
  $enabled         = true,
  $server          = 'puppet.example.com',
  $common_loglevel = 'warning',
  $agent_loglevel  = undef,
  $apply_loglevel  = undef,
) {

With this declaration, we now have seven parameters in our class that we can use in a template. You’ll notice that we explicitly set the agent and apply log levels to an undefined value. We’ll use these only if values are passed in when the class is declared. (If we left out a default value, then the parameters would be required, which is the opposite of our intention.)

There are two different template parsers within Puppet:

  • Puppet EPP templates use Puppet variables and Puppet functions.
  • Ruby ERB templates use Ruby language and functions.

It doesn’t matter which one you use, so we’ll teach you to use each of these.

Common Syntax

Both Puppet EPP and Ruby ERB files are plain-text files that contain some common syntax and template tags. Outside of these tags is normal, unprocessed text. Within the tags are Puppet or Ruby variables and code:

<%= variable or code %> This tag is replaced with the value of the variable, or result of the code.
<% code block %> The code is executed. Nothing is returned unless the code prints output.
<%# a comment %> This tag is removed from the output.
<% code block -%> Immediately trailing newlines or whitespace is removed. Use to prevent blank lines.
<%- code block %> Leading newlines or whitespace is removed. Use when indenting template tags.
<%= variable or code -%> Removes trailing whitespace after the result of the code. (There is no option for trimming leading whitespace.)
<%%
%%>
Double percent signs are replaced with a single percent sign. Use to prevent interpolation.

Don’t worry about memorizing these patterns. These tags will be used extensively in the next few pages, and you’ll get to see them applied in context.

Using Puppet EPP Templates

Adjust the file declaration from the previous section to remove the source attribute and replace it with a content attribute. The content will be provided by the epp() function:

file { '/etc/puppetlabs/puppet/puppet.conf':
  ensure  => 'file',
  owner   => 'root',
  group   => 'wheel',
  mode    => '0644',
  content => epp('puppet/puppet.conf.epp'),
}

The epp() function takes a two arguments:

Path to the Puppet EPP template
The format of that path should be modulename/filename.epp. The file should be placed in the templates/ directory of the module. Puppet EPP templates should end with the .epp extension to indicate that the file contains tags for the Puppet EPP template processor.
A hash of parameters for input
An optional hash of input parameters for the template, described in “Providing parameters”.

Let’s create a template file. From within the module directory, invoke your editor like so:

[vagrant@client puppet]$ $EDITOR templates/puppet.conf.epp

The template should look something like this:

# Generated by Puppet EPP template processor
[master]
    log_level = <%= $::puppet::common_loglevel %>

# This is used by "puppet agent"
[agent]
<% if $puppet::agent_loglevel != undef { -%>
    log_level = <%= $::puppet::agent_loglevel %>
<% } -%>
    server = <%= $::puppet::server %>

# This is used for "puppet apply"
[user]
<% if $puppet::apply_loglevel != undef { -%>
    log_level = <%= $::puppet::apply_loglevel %>
<% } -%>

This example utilizes four variables. Each instance of <%= $::class::variable %> is replaced with the Puppet variable from that class. We added those variables to the manifest as our very first step.

In this declaration, the epp() function processes the EPP template and provides the content to be placed in the file. It does this by replacing the variable lookups in the file we created with variables from the current variable scope (within this class).

Go ahead and test this change right now with puppet apply. You will see the contents of the Puppet configuration file get updated:

[vagrant@client test]$ sudo puppet apply --environment test manifests/
Info: Applying configuration version '1441617320'
Info: Computing checksum on file /etc/puppetlabs/puppet/puppet.conf
Info: /Stage[main]/Puppet/File[/etc/puppetlabs/puppet/puppet.conf]: Filebucketed
  /etc/puppetlabs/puppet/puppet.conf to puppet with sum 1698e7d7bfb88eace241ca
Notice: /Stage[main]/Puppet/File[/etc/puppetlabs/puppet/puppet.conf]/content: 
  changed '{md5}1698e7d7bfb88eace241ca' to '{md5}ae81b356db7fcf0846901d'
Notice: Applied catalog in 0.35 seconds

EPP templates can do far more than variable replacement. You can put any Puppet function within <% ... %> tags without the equals sign. Here’s an example that uses conditional evaluation to limit duplicate assignment of log levels that aren’t different:

[user]
<% if $::puppet::apply_loglevel != undef and
      $::puppet::apply_loglevel != $::puppet::common_loglevel { -%>
    log_level = <%= $::puppet::apply_loglevel %>
<% } -%>

By placing this line of output within the Puppet if condition, we ensure that the line will only be output if both conditions match. This will avoid outputting the configuration line if the log level matches the main log level, thus simplifying the configuration file.

Providing parameters

It was pretty annoying to have to list the fully qualified name of each variable, wasn’t it? You can simplify the variable naming by providing a hash of input parameters for the template. This is very useful when the Puppet variable names don’t match the variable names used in the template. For example:

  content => epp('puppet/puppet.conf.epp', {
    'server'          => $server,
    'common_loglevel' => $common_loglevel,
    'agent_loglevel'  => $agent_loglevel,
    'apply_loglevel'  => $apply_loglevel,
  }),

When providing parameter input, make sure the very first line of the template contains the following special syntax for accepting the variables:

<%- | String $server,
      String $common_loglevel,
      Optional['String'] $agent_loglevel = undef,
      Optional['String'] $apply_loglevel = undef,
| -%>

This input format exactly matches the input assignments used for class parameters, where the server and common_loglevel values are required to be provided, while the other log-level parameters are optional.

The hash of values passed to a template must match the definition at the top of the template. If it doesn’t supply all required values, an error will be issued. If it supplies too many values, a different error will be issued.

Best Practice

Always place the parameters you will use in the template at the top, and send them explicitly when declaring the template file in your manifest. This ensures the greatest readability. A reader can identify the origin of all template values from the manifest source.

Iterating over values with EPP

You can use any Puppet function within the template tags. By far the most common operation to perform within a template is to iterate through some values.

Here’s an example where we use the reduce() function to iterate through an array of tags used to limit the resources applied. This example uses the sprintf() function creatively to add commas between values:

[agent]
    tags = <%= $taglist.reduce |$tags,$tagname| {
      sprintf("%s,%s", $tags, $tagname)
    } -%>

This iteration function was used as shown in “Looping Through Iterations”.

Learning more about EPP

EPP templates are new and thus only modules designed for Puppet 4 will use them.

Complete documentation for EPP templates is available at “Puppet Functions: epp” on the Puppet docs site.

Using Ruby ERB Templates

Although EPP modules are superior, modules that provide backward compatibility for earlier versions of Puppet may need to utilize ERB templates.

Remove the epp() function and replace it with a template() function. Rename the file to use an .erb extension:

file { '/etc/puppetlabs/puppet/puppet.conf':
  ensure  => ensure,
  owner   => 'root',
  group   => 'wheel',
  mode    => '0644',
  content => template('puppet/puppet.conf.erb'),
}

Similar to the epp() function, the template() function takes a single argument: the URI of the ERB template.

Let’s create the ERB template file. The template should be placed in the templates/ directory of the module. ERB templates should end with the .erb extension to indicate that the file contains tags for the ERB template processor:

[vagrant@client puppet]$ $EDITOR templates/puppet.conf.erb

Accessing Puppet variables with ERB

Similar to the EPP example, we’ll utilize the values of Puppet variables to customize the file. Each instance of <%= @variable %> is replaced with the value of the Puppet variable named after the @ sign. Unlike EPP, there is not an explicit list of variables for the template. The variables accessed with the @ prefix must exist in the same scope (e.g., within the module class) as the template declaration.

The contents of the file should look like this:

# Generated by Puppet ERB template processor
[main]
    log_level = <%= @common_loglevel %>

# This is used by "puppet agent"
[agent]
<% if @agent_loglevel -%>
    log_level = <%= @agent_loglevel %>
<% end -%>
    server = <%= @server -%>

# This is used for "puppet apply"
[user]
<% if @apply_loglevel -%>
    log_level = <%= @apply_loglevel %>
<% end -%>

Look up variables from another class using the scope.lookupvar() function, or use scope[] as if it were a hash. For example, if we wanted to look up the log level used by MCollective, either of the following would work:

  loglevel = <%= scope.lookupvar('mcollective::loglevel') -%>
  loglevel = <%= scope['::mcollective::loglevel'] -%>

Best Practice

Avoid looking up data outside the module within a template, as it hides the external dependency from a code review of the manifest. Instead, lookup the data within the manifest and provide the data to the template in variables.

Call Puppet functions using scope.call_function(puppet_function, [params]). This takes two arguments:

  • The name of the function
  • An array of input values

For example, you could call the fqdn_rand() function to produce a random number based on the node’s hostname:

  server = <%= scope.call_function('fqdn_rand', ['3600']) -%>

As ERB templates were intended for inline Ruby development, you can put any Ruby statement within <% ... %> tags without the equals sign. Here’s an example that would limit duplicate assignment of the same log level:

[user]
<% if @apply_loglevel != @loglevel -%>
  log_level = <%= @apply_loglevel %>
<% end -%>

By wrapping this line of the template within the Ruby block, it will skip the output line if the log level matches the main log level, thus simplifying the configuration file output.

Go ahead and test this change right now with puppet apply. You will see the contents of the Puppet configuration file get updated.

Iterating over values with ERB

Here’s an example where we use the Ruby each() function to iterate through an array of tags, which can be used to limit the resources evaluated. This example uses the dash creatively to suppress line feeds and output tags as a single comma-separated value:

[agent]
    tags = <%= tags = ''; @taglist.each do |tagname|
        tags += tagname + ','
      end
      tags.chop # remove trailing comma
    %>

You’ll note that we don’t put an @ sign before the variable name. That is because we are not referencing a variable in the Puppet module class, but instead from the local loop shown in this example.

Learning more about ERB

More documentation for using ERB templates with Puppet can be found at “Embedded Ruby (ERB) Template Syntax” on the Puppet docs site.

ERB is commonly used by Ruby in many other situations, so you can find advice and help for using ERB syntax with any search engine.

Creating Readable Templates

Notice that both the original template and the preceding block sometimes utilize a leading dash in the closure: -%>. The dash tells the interpreter to suppress an immediately following line feed. This is commonly used to keep comments or Ruby statements from adding blank lines to the output, but can also be used to concatenate two sequential lines.

You want to avoid trimming whitespace with the dash if that is the end of the line, or two lines will be joined together.

Best Practice

Use Puppet EPP templates with Puppet configuration language in both the manifests and the templates. Specify parameters for the template explicitly for clarity and readability.

I cannot emphasize the preceding suggestion strongly enough. I can’t tell you how many hours I have spent searching through manifests to determine where a variable used in a template was sourced from.

Testing the Module

Now that we have created the resources, and defined our Hiera data, it’s time to test the module. We expect to see all the resources change status:

$ puppet apply --environment test ../manifests/
Notice: Compiled catalog for client.example.com in environment test
Notice: /Stage[main]/Puppet/Service[puppet]/ensure:
  ensure changed 'running' to 'stopped'
Notice: /Stage[main]/Puppet/Service[puppet]/enable:
  enable changed 'true' to 'false'
Notice: Applied catalog in 31.98 seconds

If you don’t see any of the above, it could be that the services are already configured that way (no changes) or that the values weren’t defined in Hiera correctly.

You can easily test this by changing the Hiera file and observing the change in output. For example, if you set enabled to true, you should get the following output when reapplying the class:

$ puppet apply --environment test ../manifests/
Notice: Compiled catalog for client.example.com in environment test
Notice: /Stage[main]/Puppet/Service[puppet]/enable:
  enable changed 'false' to 'true'
Notice: Applied catalog in 0.35 seconds

Peeking Beneath the Hood

In this section, we’re going to talk about peeking beneath the hood: looking at variables and resources in other classes.

Best Practice

Don’t use the techniques described here for anything other than debugging.

Environments are strict. You cannot see data or code that is not loaded in your active environment.

Within an environment, class boundaries are not enforced. You can access both variables and resources in other classes. Here’s an example:

class other_class( $idea = 'games!' ) {
  $sentence = "an idea: ${idea}"
  notify { 'announcement': message => "I have ${sentence}" }
}

class my_class {
  # I can see the other class parameter
  notice( "The idea was: ${Class['other_class']['idea']}" )

  # I can see the other class variables
  notice( "The sentence was: ${::other_class::sentence}" )

  # I can see parameters of resources in another class
  notice( "Entire message was: ${Notify['announcement']['message']}" )
}

Given an idea of games! you’d see output like this:

$ puppet apply /vagrant/manifests/peek.pp
Notice: Scope(Class[My_class]): The idea was: games!
Notice: Scope(Class[My_class]): The sentence was: an idea: games!
Notice: Scope(Class[My_class]): Entire message was: I have an idea: games!

Best Practices for Module Design

Let’s review some of the best practices for module development we covered in this chapter:

  • Create a module skeleton that implements your organization’s standards for module design.
  • Declare a class with the module name in manifests/init.pp.
  • Assign default parameter values that reflect the most likely use case.
  • Use EPP templates with explicitly provided parameter values to improve readability.
  • Don’t access external data within a template. Assign the external data to a variable in the manifest, where the dependency is visible during code review.

You can find more detailed guidelines in the Puppet Labs Style Guide.

Reviewing Custom Modules

Modules provide an independent namespace for reusable blocks of code that configure or maintain something. A module provides new resource types that can be independently used by others.

In this chapter, we discussed how to:

  • Select an appropriate module name.
  • Generate an empty module skeleton.
  • Create the default class manifest.
  • Synchronize files and directories to nodes.
  • Customize files from templates and node data.

This chapter covered the required pieces of modules, and their most common use cases. In the next chapter, we’re going to look at expert features and improved functionality you can utilize within your module.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset