Iteration in templates

If we can generate parts of a file from Puppet expressions, and also include or exclude parts of the file depending on conditions, could we generate parts of the file with a Puppet loop? That is to say, can we iterate over an array or hash, generating template content for each element? Indeed we can. This very powerful mechanism enables us to generate files of arbitrary size, based on Puppet variables, or Hiera and Facter data.

Iterating over Facter data

Our first example generates part of the config file for an application that captures network packet data. To tell it which interfaces to listen on, we need to generate a list of all the live network interfaces on the node.

How can we generate this output? We know Facter can give us a list of all the network interfaces available, with $facts['networking']['interfaces']. This is actually a hash, where the key is the name of the interface and the value is a hash of the interface's attributes, such as the IP address and netmask.

You may recall from Chapter 5, Variables, expressions, and facts, that in order to iterate over a hash, we use a syntax like the following:

HASH.each | KEY, VALUE | {
  BLOCK
}

So let's apply this pattern to the Facter data and see what the output looks like (template_iterate.epp):

<% $facts['networking']['interfaces'].each |String $interface, Hash $attrs| { -%>
interface <%= $interface %>;
<% } -%>

Each time round the loop, the values of $interface and $attrs will be set to the next key and value of the hash returned by $facts['networking']['interfaces']. As it happens, we will not be using the value of $attrs, but we still need to declare it as part of the loop syntax.

Each time round the loop, the value of $interface is set to the name of the next interface in the list, and a new output line such as the following is generated:

interface em1;

At the end of the loop, we have generated as many output lines as we have interfaces, which is the desired result. Here's the final output:

interface em1;
interface em2;
interface em3;
interface em4;
interface em5;
interface lo;

Iterating over structured facts

The next configuration data required for our application is a list of IP addresses associated with the node, which we can generate in a way similar to the previous example.

We can use more or less the same Puppet code as in the previous example; only this time we will be using each interface's $attrs hash to get the IP address of the associated interface.

The following example shows how this works (template_iterate2.epp):

<% $facts['networking']['interfaces'].each |String $interface, Hash $attrs| { -%>
local_address<%= $attrs['bindings'][0]['address'] %>;
<% } -%>

The loop is the same as in the previous example, but this time each output line contains not the value of $interface but the value of $attrs['bindings'][0]['address'], which contains the IP address of each interface.

Here's the final output:

local_address 10.170.81.11;
local_address 75.76.222.21;
local_address 204.152.248.213;
local_address 66.32.100.81;
local_address 189.183.255.6;
local_address 127.0.0.1;

Iterating over Hiera data

In Chapter 6, Managing data with Hiera, we used a Hiera array of users to generate Puppet resources for each user. Let's use the same Hiera data now to build a dynamic configuration file using iteration in a template.

The SSH daemon sshd can be configured to allow SSH access only by a list of named users (with the AllowUsers directive); indeed, it's good practice to do this.

Tip

Security tip

Most servers accessible from the public Internet regularly receive brute-force login attempts for random usernames, and dealing with these can use up a lot of resources. If sshd is configured to allow only specified users, it can quickly reject any users not in this list, without having to process the request further.

If our users are listed in Hiera, then it's easy to use a template to generate this AllowUsers list for the sshd_config file.

Just as we did when generating Puppet user resources, we will make a call to lookup() to get the array of users and iterate over this using each. The following example shows what this looks like in the template (template_hiera.epp):

AllowUsers<% lookup('users').each | $user | { -%>
 <%= $user -%>
<% } %>

Note the leading space in the second line, which results in the usernames in the output being space-separated. Note also the use of the leading hyphen to the closing tag (-%>), which, as we saw earlier in the chapter, will suppress any trailing whitespace on the line.

Here's the result:

AllowUsers katy lark bridget hsing-hui charles

Working with templates

One potential problem with templates (since they can include Puppet code, variables, and Hiera data) is that it's not always clear from the Puppet manifest what variables the template is going to use. Conversely, it's not easy to see from the template code where any referenced variables are coming from. This can make it hard to maintain or update templates, and also to debug any problems caused by incorrect data being fed into the template.

Ideally, we would like to be able to specify in the Puppet code exactly what variables the template is going to receive, and this list would also appear in the template itself. For extra credit, we would like to be able to specify the data type of input variables, in just the same way as we do for classes and defined resource types (see Chapter 8, Classes, roles, and profiles for more about this).

The good news is that EPP templates allow you to declare the parameters you want passed to your template, along with the required data types, in exactly the same way as you can for classes. While it's not compulsory to declare parameters for your EPP templates, it's a very good idea to do so. With declared and typed parameters, you will be able to catch most data errors at the template compilation stage, which makes troubleshooting much easier.

Passing parameters to templates

To declare parameters for a template, list them between pipe characters (|) inside a non-printing tag, as shown in the following example (template_params.epp):

<% | String[1] $aws_access_key,
     String[1] $aws_secret_key,
| -%>
aws_access_key_id = <%= $aws_access_key %>
aws_secret_access_key = <%= $aws_secret_key %>

When you declare parameters in a template, you must pass those parameters explicitly, in hash form, as the second argument to the epp() function call. The following example shows how to do this (epp_params.pp):

file { '/root/aws_credentials':
  content => epp('/vagrant/examples/template_params.epp',
    {
      'aws_access_key' => 'AKIAIAF7V6N2PTOIZVA2',
      'aws_secret_key' => '7IBpXjoYRVbJ/rCTVLaAMyud+i4co11lVt1Df1vt',
    }
  ),
}

This form of the epp() function call takes two parameters—the path to the template file, and a hash containing all the required template parameters. The keys to the hash are the parameter names, and the values are the values. (These need not be literal values; they could be Hiera lookups, for example.)

It's very likely that you will be using Hiera data in templates. Also, although in our previous AllowUsers example we called lookup() directly from the template to look up the data, this isn't really the best way to do it. Now that we know how to declare and pass parameters to templates, we should do the same thing with Hiera data.

Here is an updated version of the AllowUsers example, where we do the Hiera lookup in the manifest as part of the epp() call. First, we need to declare a $users parameter in the template (template_hiera_params.epp):

<% | Array[String] $users | -%>
AllowUsers<% $users.each | $user | { -%>
 <%= $user -%>
<% } %>

Then, when we compile the template with epp(), we pass in the Hiera data by calling lookup() in the parameter's hash (epp_hiera.pp):

file { '/tmp/sshd_config_example':
  content => epp('/vagrant/examples/template_hiera_params.epp',
    {
      'users' => lookup('users'),
    }
  ),
}

If you have declared a parameter list in the template, you must pass it exactly those parameters in the epp() call, and no others. EPP templates declare parameters in the same way as classes do: parameters can be given default values, and any parameter without a default value is mandatory.

It's clear from the previous example that declaring parameters makes it much easier to see what information the template is going to use from the calling code, and we now have the benefit of automated checking of the parameters and their types.

Note, however, that even templates with a parameter list can still access any Puppet variable or fact in the template body; Puppet does not prevent the template from using variables that have not been declared as parameters, or getting data directly from Hiera. It should be clear by now, though, that bypassing the parameter checking machinery in this way is a bad idea.

Tip

Best practices

Use EPP templates for dynamically-generated files, declare typed parameters in the template, and pass those parameters as a hash to the epp() function. To make your template code easier to understand and maintain, always pass data explicitly to the template. If the template needs to look up Hiera data, do the lookup in your Puppet manifest and have the template declare a parameter to receive the data.

Validating template syntax

We've seen in this chapter that templates can contain complex logic and iteration that can generate almost any output required. The downside of this power and flexibility is that it can be difficult to read and debug template code.

Fortunately, Puppet includes a tool to check and validate your templates on the command line: puppet epp validate. To use it, run the following command against your template file:

puppet epp validate /vagrant/examples/template_params.epp

If there is no output, the template is valid. If the template contains an error, you will see an error message, like the following:

Error: Syntax error at '%' at /vagrant/examples/template_params.epp:3:4
Error: Errors while validating epp
Error: Try 'puppet help epp validate' for usage

Rendering templates on the command line

As any programmer knows, even programs with valid syntax don't necessarily produce the correct results. It can be very useful to see exactly what output the template is going to produce, and Puppet also provides a tool to do this: puppet epp render.

To use it, run the following command:

puppet epp render --values "{ 'aws_access_key' => 'foo', 'aws_secret_key' => 'bar' }" /vagrant/examples/template_params.epp
aws_access_key_id = foo
aws_secret_access_key = bar

The --values argument allows you to pass in a hash of parameter-value pairs, just as you would when calling the epp() function in your Puppet manifest.

Alternatively, you can use the --values_file argument to reference a Puppet manifest file containing the hash of parameters:

echo "{ 'aws_access_key' => 'foo', 'aws_secret_key' => 'bar' }" >params.pp
puppet epp render --values_fileparams.pp /vagrant/examples/template_params.epp
aws_access_key_id = foo
aws_secret_access_key = bar

You can pass parameters both on the command line, with --values, and from a file with --values_file, simultaneously. Parameters given on the command line will take priority over those from the file:

puppet epp render --values_fileparams.pp --values "{ 'aws_access_key' => 'override' }" /vagrant/examples/template_params.epp
aws_access_key_id = override
aws_secret_access_key = bar

You can also use puppet epp render to test inline template code, using the -e switch to pass in a literal template string:

puppet epp render --values "{ 'name' => 'Dave' }" -e 'Hello, <%= $name %>'
Hello, Dave

Just as when testing your manifests, you can also use puppet apply to test your templates directly, using a command similar to the following:

sudo puppet apply -e "file { '/tmp/result': content => epp('/vagrant/examples/template_iterate.epp')}"

One advantage of this approach is that all Puppet variables, facts, and Hiera data will be available to your template.

Legacy ERB templates

You'll probably come across references to a different type of Puppet template in older code and documentation: the ERB template. ERB (short for Embedded Ruby) was the only template mechanism provided in Puppet up until version 3.5, when EPP support was added, and EPP has now replaced ERB as Puppet's default template format.

ERB template syntax looks quite similar to EPP. The following example is a snippet from an ERB template:

AllowUsers<%= @users.join(' ') %><%= scope['::vagrant'] == 'yes' ? ',vagrant' : '' %>

The difference is that the template language inside the tags is Ruby and not Puppet. Early versions of Puppet were rather limited in language features (for example, there was no each function to iterate over variables), so it was common to use Ruby code embedded in templates to work around this.

This required some complicated plumbing to manage the interface between Puppet and Ruby; for example, accessing variables in non-local scope in ERB templates requires the use of the scope hash, as in the previous example. Similarly, in order to access Puppet functions such as strftime(), you have to call:

scope.call_function('strftime', ...)

ERB templates also do not support declared parameters or type checking. I recommend you use only EPP templates in your own code.

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

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