Chapter 4. Working with Files and Packages
 | "A writer has the duty to be good, not lousy; true, not false; lively, not dull; accurate, not full of error." |  |
 | --E.B. White |
In this chapter, we will cover the following recipes:
- Making quick edits to config files
- Editing INI style files with puppetlabs-inifile
- Using Augeas to reliably edit config files
- Building config files using snippets
- Using ERB templates
- Using array iteration in templates
- Using EPP templates
- Using GnuPG to encrypt secrets
- Installing packages from a third-party repository
- Comparing package versions
Introduction
In this chapter, we'll see how to make small edits to files, how to make larger changes in a structured way using the Augeas tool, how to construct files from concatenated snippets, and how to generate files from templates. We'll also learn how to install packages from additional repositories, and how to manage those repositories. In addition, we'll see how to store and decrypt secret data with Puppet.
Making quick edits to config files
When you need to have Puppet change a particular setting in a config file, it's common to simply deploy the whole file with Puppet. This isn't always possible, though; especially if it's a file that several different parts of your Puppet manifest may need to modify.
What would be useful is a simple recipe to add a line to a config file if it's not already present, for example, adding a module name to /etc/modules
to tell the kernel to load that module at boot. There are several ways to do this, the simplest is to use the file_line
type provided by the puppetlabs-stdlib
module. In this example, we install the stdlib
module and use this type to append a line to a text file.
Getting ready
Install the puppetlabs-stdlib
module using puppet:
t@mylaptop ~ $ puppet module install puppetlabs-stdlib
Notice: Preparing to install into /home/thomas/.puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/thomas/.puppet/modules
└── puppetlabs-stdlib (v4.5.1)
This installs the module from the forge into my user's puppet directory; to install into the system directory, run the command as root or use sudo
. For the purpose of this example, we'll continue working as our own user.
How to do it...
Using the file_line
resource type, we can ensure that a line exists or is absent in a config file. Using file_line
we can quickly make edits to files without controlling the entire file.
- Create a manifest named
oneline.pp
that will usefile_line
on a file in/tmp
:file {'/tmp/cookbook': ensure => 'file', } file_line {'cookbook-hello': path => '/tmp/cookbook', line => 'Hello World!', require => File['/tmp/cookbook'], }
- Run
puppet apply
on theoneline.pp
manifest:t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp Notice: Compiled catalog for mylaptop in environment production in 0.39 seconds Notice: /Stage[main]/Main/File[/tmp/cookbook]/ensure: created Notice: /Stage[main]/Main/File_line[cookbook-hello]/ensure: created Notice: Finished catalog run in 0.02 seconds
- Now verify that
/tmp/cookbook
contains the line we defined:t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook Hello World!
How it works…
We installed the puppetlabs-stdlib
module into the default module path for Puppet, so when we ran puppet apply
, Puppet knew where to find the file_line
type definition. Puppet then created the /tmp/cookbook
file if it didn't exist. The line Hello World!
was not found in the file, so Puppet added the line to the file.
There's more…
We can define more instances of file_line
and add more lines to the file; we can have multiple resources modifying a single file.
Modify the oneline.pp
file and add another file_line
resource:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-hello':
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-goodbye':
path => '/tmp/cookbook',
line => 'So long, and thanks for all the fish.',
require => File['/tmp/cookbook'],
}
Now apply the manifest again and verify whether the new line is appended to the file:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.36 seconds
Notice: /Stage[main]/Main/File_line[cookbook-goodbye]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
The file_line
type also supports pattern matching and line removal as we'll show you in the following example:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-remove':
ensure => 'absent',
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-match':
path => '/tmp/cookbook',
line => 'Oh freddled gruntbuggly, thanks for all the fish.',
match => 'fish.$',
require => File['/tmp/cookbook'],
}
Verify the contents of /tmp/cookbook
before your Puppet run:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
Apply the updated manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.30 seconds
Notice: /Stage[main]/Main/File_line[cookbook-match]/ensure: created
Notice: /Stage[main]/Main/File_line[cookbook-remove]/ensure: removed
Notice: Finished catalog run in 0.02 seconds
Verify that the line has been removed and the goodbye line has been replaced:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Oh freddled gruntbuggly, thanks for all the fish.
Editing files with file_line
works well if the file is unstructured. Structured files may have similar lines in different sections that have different meanings. In the next section, we'll show you how to deal with one particular type of structured file, a file using INI syntax.
puppetlabs-stdlib
module
using puppet:
t@mylaptop ~ $ puppet module install puppetlabs-stdlib
Notice: Preparing to install into /home/thomas/.puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/thomas/.puppet/modules
└── puppetlabs-stdlib (v4.5.1)
This installs the module from the forge into my user's puppet directory; to install into the system directory, run the command as root or use sudo
. For the purpose of this example, we'll continue working as our own user.
How to do it...
Using the file_line
resource type, we can ensure that a line exists or is absent in a config file. Using file_line
we can quickly make edits to files without controlling the entire file.
- Create a manifest named
oneline.pp
that will usefile_line
on a file in/tmp
:file {'/tmp/cookbook': ensure => 'file', } file_line {'cookbook-hello': path => '/tmp/cookbook', line => 'Hello World!', require => File['/tmp/cookbook'], }
- Run
puppet apply
on theoneline.pp
manifest:t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp Notice: Compiled catalog for mylaptop in environment production in 0.39 seconds Notice: /Stage[main]/Main/File[/tmp/cookbook]/ensure: created Notice: /Stage[main]/Main/File_line[cookbook-hello]/ensure: created Notice: Finished catalog run in 0.02 seconds
- Now verify that
/tmp/cookbook
contains the line we defined:t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook Hello World!
How it works…
We installed the puppetlabs-stdlib
module into the default module path for Puppet, so when we ran puppet apply
, Puppet knew where to find the file_line
type definition. Puppet then created the /tmp/cookbook
file if it didn't exist. The line Hello World!
was not found in the file, so Puppet added the line to the file.
There's more…
We can define more instances of file_line
and add more lines to the file; we can have multiple resources modifying a single file.
Modify the oneline.pp
file and add another file_line
resource:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-hello':
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-goodbye':
path => '/tmp/cookbook',
line => 'So long, and thanks for all the fish.',
require => File['/tmp/cookbook'],
}
Now apply the manifest again and verify whether the new line is appended to the file:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.36 seconds
Notice: /Stage[main]/Main/File_line[cookbook-goodbye]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
The file_line
type also supports pattern matching and line removal as we'll show you in the following example:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-remove':
ensure => 'absent',
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-match':
path => '/tmp/cookbook',
line => 'Oh freddled gruntbuggly, thanks for all the fish.',
match => 'fish.$',
require => File['/tmp/cookbook'],
}
Verify the contents of /tmp/cookbook
before your Puppet run:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
Apply the updated manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.30 seconds
Notice: /Stage[main]/Main/File_line[cookbook-match]/ensure: created
Notice: /Stage[main]/Main/File_line[cookbook-remove]/ensure: removed
Notice: Finished catalog run in 0.02 seconds
Verify that the line has been removed and the goodbye line has been replaced:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Oh freddled gruntbuggly, thanks for all the fish.
Editing files with file_line
works well if the file is unstructured. Structured files may have similar lines in different sections that have different meanings. In the next section, we'll show you how to deal with one particular type of structured file, a file using INI syntax.
file_line
resource type, we can ensure that a line exists or is absent in a config file. Using file_line
we can quickly make edits to files without controlling the entire file.
oneline.pp
that will use file_line
on a file in /tmp
:file {'/tmp/cookbook': ensure => 'file', } file_line {'cookbook-hello': path => '/tmp/cookbook', line => 'Hello World!', require => File['/tmp/cookbook'], }
puppet apply
on the oneline.pp
manifest:t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp Notice: Compiled catalog for mylaptop in environment production in 0.39 seconds Notice: /Stage[main]/Main/File[/tmp/cookbook]/ensure: created Notice: /Stage[main]/Main/File_line[cookbook-hello]/ensure: created Notice: Finished catalog run in 0.02 seconds
/tmp/cookbook
contains the line we defined:t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook Hello World!
How it works…
We installed the puppetlabs-stdlib
module into the default module path for Puppet, so when we ran puppet apply
, Puppet knew where to find the file_line
type definition. Puppet then created the /tmp/cookbook
file if it didn't exist. The line Hello World!
was not found in the file, so Puppet added the line to the file.
There's more…
We can define more instances of file_line
and add more lines to the file; we can have multiple resources modifying a single file.
Modify the oneline.pp
file and add another file_line
resource:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-hello':
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-goodbye':
path => '/tmp/cookbook',
line => 'So long, and thanks for all the fish.',
require => File['/tmp/cookbook'],
}
Now apply the manifest again and verify whether the new line is appended to the file:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.36 seconds
Notice: /Stage[main]/Main/File_line[cookbook-goodbye]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
The file_line
type also supports pattern matching and line removal as we'll show you in the following example:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-remove':
ensure => 'absent',
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-match':
path => '/tmp/cookbook',
line => 'Oh freddled gruntbuggly, thanks for all the fish.',
match => 'fish.$',
require => File['/tmp/cookbook'],
}
Verify the contents of /tmp/cookbook
before your Puppet run:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
Apply the updated manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.30 seconds
Notice: /Stage[main]/Main/File_line[cookbook-match]/ensure: created
Notice: /Stage[main]/Main/File_line[cookbook-remove]/ensure: removed
Notice: Finished catalog run in 0.02 seconds
Verify that the line has been removed and the goodbye line has been replaced:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Oh freddled gruntbuggly, thanks for all the fish.
Editing files with file_line
works well if the file is unstructured. Structured files may have similar lines in different sections that have different meanings. In the next section, we'll show you how to deal with one particular type of structured file, a file using INI syntax.
the puppetlabs-stdlib
module into the default module path for Puppet, so when we ran puppet apply
, Puppet knew where to find the file_line
type definition. Puppet then created the /tmp/cookbook
file if it didn't exist. The line Hello World!
was not found in the file, so Puppet added the line to the file.
There's more…
We can define more instances of file_line
and add more lines to the file; we can have multiple resources modifying a single file.
Modify the oneline.pp
file and add another file_line
resource:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-hello':
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-goodbye':
path => '/tmp/cookbook',
line => 'So long, and thanks for all the fish.',
require => File['/tmp/cookbook'],
}
Now apply the manifest again and verify whether the new line is appended to the file:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.36 seconds
Notice: /Stage[main]/Main/File_line[cookbook-goodbye]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
The file_line
type also supports pattern matching and line removal as we'll show you in the following example:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-remove':
ensure => 'absent',
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-match':
path => '/tmp/cookbook',
line => 'Oh freddled gruntbuggly, thanks for all the fish.',
match => 'fish.$',
require => File['/tmp/cookbook'],
}
Verify the contents of /tmp/cookbook
before your Puppet run:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
Apply the updated manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.30 seconds
Notice: /Stage[main]/Main/File_line[cookbook-match]/ensure: created
Notice: /Stage[main]/Main/File_line[cookbook-remove]/ensure: removed
Notice: Finished catalog run in 0.02 seconds
Verify that the line has been removed and the goodbye line has been replaced:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Oh freddled gruntbuggly, thanks for all the fish.
Editing files with file_line
works well if the file is unstructured. Structured files may have similar lines in different sections that have different meanings. In the next section, we'll show you how to deal with one particular type of structured file, a file using INI syntax.
file_line
and add more lines to the file; we can have multiple resources modifying a single file.
oneline.pp
file
and add another file_line
resource:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-hello':
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-goodbye':
path => '/tmp/cookbook',
line => 'So long, and thanks for all the fish.',
require => File['/tmp/cookbook'],
}
Now apply the manifest again and verify whether the new line is appended to the file:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.36 seconds
Notice: /Stage[main]/Main/File_line[cookbook-goodbye]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
The file_line
type also supports pattern matching and line removal as we'll show you in the following example:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-remove':
ensure => 'absent',
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-match':
path => '/tmp/cookbook',
line => 'Oh freddled gruntbuggly, thanks for all the fish.',
match => 'fish.$',
require => File['/tmp/cookbook'],
}
Verify the contents of /tmp/cookbook
before your Puppet run:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
Apply the updated manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.30 seconds
Notice: /Stage[main]/Main/File_line[cookbook-match]/ensure: created
Notice: /Stage[main]/Main/File_line[cookbook-remove]/ensure: removed
Notice: Finished catalog run in 0.02 seconds
Verify that the line has been removed and the goodbye line has been replaced:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Oh freddled gruntbuggly, thanks for all the fish.
Editing files with file_line
works well if the file is unstructured. Structured files may have similar lines in different sections that have different meanings. In the next section, we'll show you how to deal with one particular type of structured file, a file using INI syntax.
Editing INI style files with puppetlabs-inifile
INI files are used throughout many systems, Puppet uses INI syntax for the puppet.conf
file. The puppetlabs-inifile
module creates two types, ini_setting
and ini_subsetting
, which can be used to edit INI style files.
Getting ready
Install the module from the forge as follows:
t@mylaptop ~ $ puppet module install puppetlabs-inifile
Notice: Preparing to install into /home/tuphill/.puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/tuphill/.puppet/modules
└── puppetlabs-inifile (v1.1.3)
How to do it...
In this example, we will create a /tmp/server.conf
file and ensure that the server_true
setting is set in that file:
- Create an
initest.pp
manifest with the following contents:ini_setting {'server_true': path => '/tmp/server.conf', section => 'main', setting => 'server', value => 'true', }
- Apply the manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp Notice: Compiled catalog for burnaby in environment production in 0.14 seconds Notice: /Stage[main]/Main/Ini_setting[server_true]/ensure: created Notice: Finished catalog run in 0.02 seconds
- Verify the contents of the
/tmp/server.conf
file:t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf [main] server = true
How it works...
The inifile
module defines two types, ini_setting
and ini_subsetting
. Our manifest defines an ini_setting
resource that creates a server = true setting within the main section of the ini
file. In our case, the file didn't exist, so Puppet created the file, then created the main
section, and finally added the setting to the main
section.
There's more...
Using ini_subsetting
, you can have several resources added to a setting. For instance, our server.conf
file has a server's line, we could have each node append its own hostname to a server's line. Add the following to the end of the initest.pp
file:
ini_subsetting {'server_name':
path => '/tmp/server.conf',
section => 'main',
setting => 'server_host',
subsetting => "$hostname",
}
Apply the manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for mylaptop in environment production in 0.34 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop
Now temporarily change your hostname and rerun Puppet:
t@mylaptop ~/.puppet/manifests $ sudo hostname inihost
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for inihost in environment production in 0.43 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop inihost
Tip
When working with INI syntax files, using the inifile
module is an excellent choice.
If your configuration files are not in INI syntax, another tool, Augeas, can be used. In the following section, we will use augeas
to modify files.
module from the forge as follows:
t@mylaptop ~ $ puppet module install puppetlabs-inifile
Notice: Preparing to install into /home/tuphill/.puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/tuphill/.puppet/modules
└── puppetlabs-inifile (v1.1.3)
How to do it...
In this example, we will create a /tmp/server.conf
file and ensure that the server_true
setting is set in that file:
- Create an
initest.pp
manifest with the following contents:ini_setting {'server_true': path => '/tmp/server.conf', section => 'main', setting => 'server', value => 'true', }
- Apply the manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp Notice: Compiled catalog for burnaby in environment production in 0.14 seconds Notice: /Stage[main]/Main/Ini_setting[server_true]/ensure: created Notice: Finished catalog run in 0.02 seconds
- Verify the contents of the
/tmp/server.conf
file:t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf [main] server = true
How it works...
The inifile
module defines two types, ini_setting
and ini_subsetting
. Our manifest defines an ini_setting
resource that creates a server = true setting within the main section of the ini
file. In our case, the file didn't exist, so Puppet created the file, then created the main
section, and finally added the setting to the main
section.
There's more...
Using ini_subsetting
, you can have several resources added to a setting. For instance, our server.conf
file has a server's line, we could have each node append its own hostname to a server's line. Add the following to the end of the initest.pp
file:
ini_subsetting {'server_name':
path => '/tmp/server.conf',
section => 'main',
setting => 'server_host',
subsetting => "$hostname",
}
Apply the manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for mylaptop in environment production in 0.34 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop
Now temporarily change your hostname and rerun Puppet:
t@mylaptop ~/.puppet/manifests $ sudo hostname inihost
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for inihost in environment production in 0.43 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop inihost
Tip
When working with INI syntax files, using the inifile
module is an excellent choice.
If your configuration files are not in INI syntax, another tool, Augeas, can be used. In the following section, we will use augeas
to modify files.
/tmp/server.conf
file and ensure that the server_true
setting is set in that file:
initest.pp
manifest with the following contents:ini_setting {'server_true': path => '/tmp/server.conf', section => 'main', setting => 'server', value => 'true', }
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp Notice: Compiled catalog for burnaby in environment production in 0.14 seconds Notice: /Stage[main]/Main/Ini_setting[server_true]/ensure: created Notice: Finished catalog run in 0.02 seconds
/tmp/server.conf
file:t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf [main] server = true
How it works...
The inifile
module defines two types, ini_setting
and ini_subsetting
. Our manifest defines an ini_setting
resource that creates a server = true setting within the main section of the ini
file. In our case, the file didn't exist, so Puppet created the file, then created the main
section, and finally added the setting to the main
section.
There's more...
Using ini_subsetting
, you can have several resources added to a setting. For instance, our server.conf
file has a server's line, we could have each node append its own hostname to a server's line. Add the following to the end of the initest.pp
file:
ini_subsetting {'server_name':
path => '/tmp/server.conf',
section => 'main',
setting => 'server_host',
subsetting => "$hostname",
}
Apply the manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for mylaptop in environment production in 0.34 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop
Now temporarily change your hostname and rerun Puppet:
t@mylaptop ~/.puppet/manifests $ sudo hostname inihost
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for inihost in environment production in 0.43 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop inihost
Tip
When working with INI syntax files, using the inifile
module is an excellent choice.
If your configuration files are not in INI syntax, another tool, Augeas, can be used. In the following section, we will use augeas
to modify files.
inifile
module defines two types, ini_setting
and ini_subsetting
. Our manifest defines an ini_setting
resource that creates a server = true setting within
the main section of the ini
file. In our case, the file didn't exist, so Puppet created the file, then created the main
section, and finally added the setting to the main
section.
There's more...
Using ini_subsetting
, you can have several resources added to a setting. For instance, our server.conf
file has a server's line, we could have each node append its own hostname to a server's line. Add the following to the end of the initest.pp
file:
ini_subsetting {'server_name':
path => '/tmp/server.conf',
section => 'main',
setting => 'server_host',
subsetting => "$hostname",
}
Apply the manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for mylaptop in environment production in 0.34 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop
Now temporarily change your hostname and rerun Puppet:
t@mylaptop ~/.puppet/manifests $ sudo hostname inihost
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for inihost in environment production in 0.43 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop inihost
Tip
When working with INI syntax files, using the inifile
module is an excellent choice.
If your configuration files are not in INI syntax, another tool, Augeas, can be used. In the following section, we will use augeas
to modify files.
ini_subsetting
, you can have
several resources added to a setting. For instance, our server.conf
file has a server's line, we could have each node append its own hostname to a server's line. Add the following to the end of the initest.pp
file:
ini_subsetting {'server_name':
path => '/tmp/server.conf',
section => 'main',
setting => 'server_host',
subsetting => "$hostname",
}
Apply the manifest:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for mylaptop in environment production in 0.34 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop
Now temporarily change your hostname and rerun Puppet:
t@mylaptop ~/.puppet/manifests $ sudo hostname inihost
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for inihost in environment production in 0.43 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop inihost
Tip
When working with INI syntax files, using the inifile
module is an excellent choice.
If your configuration files are not in INI syntax, another tool, Augeas, can be used. In the following section, we will use augeas
to modify files.
Using Augeas to reliably edit config files
Sometimes it seems like every application has its own subtly different config file format, and writing regular expressions to parse and modify all of them can be a tiresome business.
Thankfully, Augeas is here to help. Augeas is a system that aims to simplify working with different config file formats by presenting them all as a simple tree of values. Puppet's Augeas support allows you to create augeas
resources that can make the required config changes intelligently and automatically.
How to do it…
Follow these steps to create an example augeas
resource:
- Modify your
base
module as follows:class base { augeas { 'enable-ip-forwarding': incl => '/etc/sysctl.conf', lens => 'Sysctl.lns', changes => ['set net.ipv4.ip_forward 1'], } }
- Run Puppet:
[root@cookbook ~]# puppet agent -t Info: Applying configuration version '1412130479' Notice: Augeas[enable-ip-forwarding](provider=augeas): --- /etc/sysctl.conf 2014-09-04 03:41:09.000000000 -0400 +++ /etc/sysctl.conf.augnew 2014-09-30 22:28:03.503000039 -0400 @@ -4,7 +4,7 @@ # sysctl.conf(5) for more details. # Controls IP packet forwarding -net.ipv4.ip_forward = 0 +net.ipv4.ip_forward = 1 # Controls source route verification net.ipv4.conf.default.rp_filter = 1 Notice: /Stage[main]/Base/Augeas[enable-ip-forwarding]/returns: executed successfully Notice: Finished catalog run in 2.27 seconds
- Check whether the setting has been correctly applied:
[root@cookbook ~]# sysctl -p |grep ip_forward net.ipv4.ip_forward = 1
How it works…
We declare an augeas
resource named enable-ip-forwarding
:
augeas { 'enable-ip-forwarding':
We specify that we want to make changes in the file /etc/sysctl.conf
:
incl => '/etc/sysctl.conf',
Next we specify the lens to use on this file. Augeas uses files called lenses to translate a configuration file into an object representation. Augeas ships with several lenses, they are located in /usr/share/augeas/lenses
by default. When specifying the lens in an augeas
resource, the name of the lens is capitalized and has the .lns
suffix. In this case, we will specify the Sysctl
lens as follows:
lens => 'Sysctl.lns',
The changes
parameter specifies the changes we want to make. Its value is an array, because we can supply several changes at once. In this example, there is only change, so the value is an array of one element:
changes => ['set net.ipv4.ip_forward 1'],
In general, Augeas changes take the following form:
set <parameter> <value>
In this case, the setting will be translated into a line like this in /etc/sysctl.conf
:
net.ipv4.ip_forward=1
There's more…
I've chosen /etc/sysctl.conf
as the example because it can contain a wide variety of kernel settings and you may want to change these settings for all sorts of different purposes and in different Puppet classes. You might want to enable IP forwarding, as in the example, for a router class but you might also want to tune the value of net.core.somaxconn
for a load-balancer class.
This means that simply puppetizing the /etc/sysctl.conf
file and distributing it as a text file won't work because you might have several different and conflicting versions depending on the setting you want to modify. Augeas is the right solution here because you can define augeas
resources in different places, which modify the same file and they won't conflict.
For more information about using Puppet and Augeas, see the page on the Puppet Labs website http://projects.puppetlabs.com/projects/1/wiki/Puppet_Augeas.
Another project that uses Augeas is Augeasproviders. Augeasproviders uses Augeas to define several types. One of these types is sysctl
, using this type you can make sysctl changes without knowing how to write the changes in Augeas. More information is available on the forge at https://forge.puppetlabs.com/domcleal/augeasproviders.
Learning how to use Augeas can be a little confusing at first. Augeas provides a command line tool, augtool
, which can be used to get acquainted with making changes in Augeas.
augeas
resource:
base
module as follows:class base { augeas { 'enable-ip-forwarding': incl => '/etc/sysctl.conf', lens => 'Sysctl.lns', changes => ['set net.ipv4.ip_forward 1'], } }
[root@cookbook ~]# puppet agent -t Info: Applying configuration version '1412130479' Notice: Augeas[enable-ip-forwarding](provider=augeas): --- /etc/sysctl.conf 2014-09-04 03:41:09.000000000 -0400 +++ /etc/sysctl.conf.augnew 2014-09-30 22:28:03.503000039 -0400 @@ -4,7 +4,7 @@ # sysctl.conf(5) for more details. # Controls IP packet forwarding -net.ipv4.ip_forward = 0 +net.ipv4.ip_forward = 1 # Controls source route verification net.ipv4.conf.default.rp_filter = 1 Notice: /Stage[main]/Base/Augeas[enable-ip-forwarding]/returns: executed successfully Notice: Finished catalog run in 2.27 seconds
[root@cookbook ~]# sysctl -p |grep ip_forward net.ipv4.ip_forward = 1
How it works…
We declare an augeas
resource named enable-ip-forwarding
:
augeas { 'enable-ip-forwarding':
We specify that we want to make changes in the file /etc/sysctl.conf
:
incl => '/etc/sysctl.conf',
Next we specify the lens to use on this file. Augeas uses files called lenses to translate a configuration file into an object representation. Augeas ships with several lenses, they are located in /usr/share/augeas/lenses
by default. When specifying the lens in an augeas
resource, the name of the lens is capitalized and has the .lns
suffix. In this case, we will specify the Sysctl
lens as follows:
lens => 'Sysctl.lns',
The changes
parameter specifies the changes we want to make. Its value is an array, because we can supply several changes at once. In this example, there is only change, so the value is an array of one element:
changes => ['set net.ipv4.ip_forward 1'],
In general, Augeas changes take the following form:
set <parameter> <value>
In this case, the setting will be translated into a line like this in /etc/sysctl.conf
:
net.ipv4.ip_forward=1
There's more…
I've chosen /etc/sysctl.conf
as the example because it can contain a wide variety of kernel settings and you may want to change these settings for all sorts of different purposes and in different Puppet classes. You might want to enable IP forwarding, as in the example, for a router class but you might also want to tune the value of net.core.somaxconn
for a load-balancer class.
This means that simply puppetizing the /etc/sysctl.conf
file and distributing it as a text file won't work because you might have several different and conflicting versions depending on the setting you want to modify. Augeas is the right solution here because you can define augeas
resources in different places, which modify the same file and they won't conflict.
For more information about using Puppet and Augeas, see the page on the Puppet Labs website http://projects.puppetlabs.com/projects/1/wiki/Puppet_Augeas.
Another project that uses Augeas is Augeasproviders. Augeasproviders uses Augeas to define several types. One of these types is sysctl
, using this type you can make sysctl changes without knowing how to write the changes in Augeas. More information is available on the forge at https://forge.puppetlabs.com/domcleal/augeasproviders.
Learning how to use Augeas can be a little confusing at first. Augeas provides a command line tool, augtool
, which can be used to get acquainted with making changes in Augeas.
augeas
resource named enable-ip-forwarding
:
augeas { 'enable-ip-forwarding':
We specify that we want to make changes in the file /etc/sysctl.conf
:
incl => '/etc/sysctl.conf',
Next we specify the lens to use on this file. Augeas uses files called lenses to translate a configuration file into an object representation. Augeas ships with several lenses, they are located in /usr/share/augeas/lenses
by default. When specifying the lens in an augeas
resource, the name of the lens is capitalized and has the .lns
suffix. In this case, we will specify the Sysctl
lens as follows:
lens => 'Sysctl.lns',
The changes
parameter specifies the changes we want to make. Its value is an array, because we can supply several changes at once. In this example, there is only change, so the value is an array of one element:
changes => ['set net.ipv4.ip_forward 1'],
In general, Augeas changes take the following form:
set <parameter> <value>
In this case, the setting will be translated into a line like this in /etc/sysctl.conf
:
net.ipv4.ip_forward=1
There's more…
I've chosen /etc/sysctl.conf
as the example because it can contain a wide variety of kernel settings and you may want to change these settings for all sorts of different purposes and in different Puppet classes. You might want to enable IP forwarding, as in the example, for a router class but you might also want to tune the value of net.core.somaxconn
for a load-balancer class.
This means that simply puppetizing the /etc/sysctl.conf
file and distributing it as a text file won't work because you might have several different and conflicting versions depending on the setting you want to modify. Augeas is the right solution here because you can define augeas
resources in different places, which modify the same file and they won't conflict.
For more information about using Puppet and Augeas, see the page on the Puppet Labs website http://projects.puppetlabs.com/projects/1/wiki/Puppet_Augeas.
Another project that uses Augeas is Augeasproviders. Augeasproviders uses Augeas to define several types. One of these types is sysctl
, using this type you can make sysctl changes without knowing how to write the changes in Augeas. More information is available on the forge at https://forge.puppetlabs.com/domcleal/augeasproviders.
Learning how to use Augeas can be a little confusing at first. Augeas provides a command line tool, augtool
, which can be used to get acquainted with making changes in Augeas.
/etc/sysctl.conf
as the example because it can contain a wide variety of kernel settings and you may want to change these settings for all sorts of different purposes and in different Puppet classes. You might want to enable IP forwarding, as in the example, for a router class but you might also want to tune the value of net.core.somaxconn
for a load-balancer class.
/etc/sysctl.conf
file and distributing it as a text file won't work because you might have several different and conflicting versions depending on the setting you want to modify. Augeas is the right solution here because you can define augeas
resources in different places, which modify the same file and they won't conflict.
Augeas, see the page on the Puppet Labs website http://projects.puppetlabs.com/projects/1/wiki/Puppet_Augeas.
Another project that uses Augeas is Augeasproviders. Augeasproviders uses Augeas to define several types. One of these types is sysctl
, using this type you can make sysctl changes without knowing how to write the changes in Augeas. More information is available on the forge at https://forge.puppetlabs.com/domcleal/augeasproviders.
Learning how to use Augeas can be a little confusing at first. Augeas provides a command line tool, augtool
, which can be used to get acquainted with making changes in Augeas.
Building config files using snippets
Sometimes you can't deploy a whole config file in one piece, yet making line by line edits isn't enough. Often, you need to build a config file from various bits of configuration managed by different classes. You may run into a situation where local information needs to be imported into the file as well. In this example, we'll build a config file using a local file as well as snippets defined in our manifests.
Getting ready
Although it's possible to create our own system to build files from pieces, we'll use the puppetlabs supported concat
module. We will start by installing the concat
module, in a previous example we installed the module to our local machine. In this example, we'll modify the Puppet server configuration and download the module to the Puppet server.
In your Git repository create an environment.conf
file with the following contents:
modulepath = public:modules
manifest = manifests/site.pp
Create the public directory and download the module into that directory as follows:
t@mylaptop ~/puppet $ mkdir public && cd public
t@mylaptop ~/puppet/public $ puppet module install puppetlabs-concat --modulepath=.
Notice: Preparing to install into /home/thomas/puppet/public ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/thomas/puppet/public
└─┬ puppetlabs-concat (v1.1.1)
└── puppetlabs-stdlib (v4.3.2)
Now add the new modules to our Git repository:
t@mylaptop ~/puppet/public $ git add .
t@mylaptop ~/puppet/public $ git commit -m "adding concat"
[production 50c6fca] adding concat
407 files changed, 20089 insertions(+)
Then push to our Git server:
t@mylaptop ~/puppet/public $ git push origin production
How to do it...
Now that we have the concat
module available on our server, we can create a concat
container resource in our base
module:
concat {'hosts.allow':
path => '/etc/hosts.allow',
mode => 0644
}
Create a concat::fragment
module for the header of the new file:
concat::fragment {'hosts.allow header':
target => 'hosts.allow',
content => "# File managed by puppet\n",
order => '01'
}
Create a concat::fragment
that includes a local file:
concat::fragment {'hosts.allow local':
target => 'hosts.allow',
source => '/etc/hosts.allow.local',
order => '10',
}
Create a concat::fragment
module that will go at the end of the file:
concat::fragment {'hosts.allow tftp':
target => 'hosts.allow',
content => "in.ftpd: .example.com\n",
order => '50',
}
On the node, create /etc/hosts.allow.local
with the following contents:
in.tftpd: .example.com
Run Puppet to have the file created:
[root@cookbook ~]# puppet agent -t
Info: Caching catalog for cookbook.example.com
Info: Applying configuration version '1412138600'
Notice: /Stage[main]/Base/Concat[hosts.allow]/File[hosts.allow]/ensure: defined content as '{md5}b151c8bbc32c505f1c4a98b487f7d249'
Notice: Finished catalog run in 0.29 seconds
Verify the contents of the new file as:
[root@cookbook ~]# cat /etc/hosts.allow
# File managed by puppet
in.tftpd: .example.com
in.ftpd: .example.com
How it works...
The concat
resource defines a container that will hold all the subsequent concat::fragment
resources. Each concat::fragment
resource references the concat
resource as the target. Each concat::fragment
also includes an order
attribute. The order
attribute is used to specify the order in which the fragments are added to the final file. Our /etc/hosts.allow
file is built with the header line, the contents of the local file, and finally the in.tftpd
line we defined.
concat
module. We will start by installing the concat
module, in a previous example we installed the module to our local machine. In this example, we'll modify the Puppet server configuration and download the module to the Puppet server.
environment.conf
file with the following contents:
How to do it...
Now that we have the concat
module available on our server, we can create a concat
container resource in our base
module:
concat {'hosts.allow':
path => '/etc/hosts.allow',
mode => 0644
}
Create a concat::fragment
module for the header of the new file:
concat::fragment {'hosts.allow header':
target => 'hosts.allow',
content => "# File managed by puppet\n",
order => '01'
}
Create a concat::fragment
that includes a local file:
concat::fragment {'hosts.allow local':
target => 'hosts.allow',
source => '/etc/hosts.allow.local',
order => '10',
}
Create a concat::fragment
module that will go at the end of the file:
concat::fragment {'hosts.allow tftp':
target => 'hosts.allow',
content => "in.ftpd: .example.com\n",
order => '50',
}
On the node, create /etc/hosts.allow.local
with the following contents:
in.tftpd: .example.com
Run Puppet to have the file created:
[root@cookbook ~]# puppet agent -t
Info: Caching catalog for cookbook.example.com
Info: Applying configuration version '1412138600'
Notice: /Stage[main]/Base/Concat[hosts.allow]/File[hosts.allow]/ensure: defined content as '{md5}b151c8bbc32c505f1c4a98b487f7d249'
Notice: Finished catalog run in 0.29 seconds
Verify the contents of the new file as:
[root@cookbook ~]# cat /etc/hosts.allow
# File managed by puppet
in.tftpd: .example.com
in.ftpd: .example.com
How it works...
The concat
resource defines a container that will hold all the subsequent concat::fragment
resources. Each concat::fragment
resource references the concat
resource as the target. Each concat::fragment
also includes an order
attribute. The order
attribute is used to specify the order in which the fragments are added to the final file. Our /etc/hosts.allow
file is built with the header line, the contents of the local file, and finally the in.tftpd
line we defined.
the concat
module available on our server, we can create a concat
container resource in our base
module:
concat {'hosts.allow':
path => '/etc/hosts.allow',
mode => 0644
}
Create a concat::fragment
module for the header of the new file:
concat::fragment {'hosts.allow header':
target => 'hosts.allow',
content => "# File managed by puppet\n",
order => '01'
}
Create a concat::fragment
that includes a local file:
concat::fragment {'hosts.allow local':
target => 'hosts.allow',
source => '/etc/hosts.allow.local',
order => '10',
}
Create a concat::fragment
module that will go at the end of the file:
concat::fragment {'hosts.allow tftp':
target => 'hosts.allow',
content => "in.ftpd: .example.com\n",
order => '50',
}
On the node, create /etc/hosts.allow.local
with the following contents:
in.tftpd: .example.com
Run Puppet to have the file created:
[root@cookbook ~]# puppet agent -t
Info: Caching catalog for cookbook.example.com
Info: Applying configuration version '1412138600'
Notice: /Stage[main]/Base/Concat[hosts.allow]/File[hosts.allow]/ensure: defined content as '{md5}b151c8bbc32c505f1c4a98b487f7d249'
Notice: Finished catalog run in 0.29 seconds
Verify the contents of the new file as:
[root@cookbook ~]# cat /etc/hosts.allow
# File managed by puppet
in.tftpd: .example.com
in.ftpd: .example.com
How it works...
The concat
resource defines a container that will hold all the subsequent concat::fragment
resources. Each concat::fragment
resource references the concat
resource as the target. Each concat::fragment
also includes an order
attribute. The order
attribute is used to specify the order in which the fragments are added to the final file. Our /etc/hosts.allow
file is built with the header line, the contents of the local file, and finally the in.tftpd
line we defined.
concat
resource defines
a container that will hold all the subsequent concat::fragment
resources. Each concat::fragment
resource references the concat
resource as the target. Each concat::fragment
also includes an order
attribute. The order
attribute is used to specify the order in which the fragments are added to the final file. Our /etc/hosts.allow
file is built with the header line, the contents of the local file, and finally the in.tftpd
line we defined.
Using ERB templates
While you can deploy config files easily with Puppet as simple text files, templates are much more powerful. A template file can do calculations, execute Ruby code, or reference the values of variables from your Puppet manifests. Anywhere you might deploy a text file using Puppet, you can use a template instead.
In the simplest case, a template can just be a static text file. More usefully, you can insert variables into it using the ERB (embedded Ruby) syntax. For example:
<%= @name %>, this is a very large drink.
If the template is used in a context where the variable $name
contains Zaphod Beeblebrox
, the template will evaluate to:
Zaphod Beeblebrox, this is a very large drink.
This simple technique is very useful to generate lots of files that only differ in the values of one or two variables, for example, virtual hosts, and for inserting values into a script such as database names and passwords.
How to do it…
In this example, we'll use an ERB template to insert a password into a backup script:
- Create the file
modules/admin/templates/backup-mysql.sh.erb
with the following contents:#!/bin/sh /usr/bin/mysqldump -uroot \ -p<%= @mysql_password %> \ --all-databases | \ /bin/gzip > /backup/mysql/all-databases.sql.gz
- Modify your
site.pp
file as follows:node 'cookbook' { $mysql_password = 'secret' file { '/usr/local/bin/backup-mysql': content => template('admin/backup-mysql.sh.erb'), mode => '0755', } }
- Run Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412140971' Notice: /Stage[main]/Main/Node[cookbook]/File[/usr/local/bin/backup-mysql]/ensure: defined content as '{md5}c12af56559ef36529975d568ff52dca5' Notice: Finished catalog run in 0.31 seconds
- Check whether Puppet has correctly inserted the password into the template:
[root@cookbook ~]# cat /usr/local/bin/backup-mysql #!/bin/sh /usr/bin/mysqldump -uroot \ -psecret \ --all-databases | \ /bin/gzip > /backup/mysql/all-databases.sql.gz
How it works…
Wherever a variable is referenced in the template, for example <%= @mysql_password %>
, Puppet will replace it with the corresponding value, secret
.
There's more…
In the example, we only used one variable in the template, but you can have as many as you like. These can also be facts:
ServerName <%= @fqdn %>
Or Ruby expressions:
MAILTO=<%= @emails.join(',') %>
Or any Ruby code you want:
ServerAdmin <%= @sitedomain == 'coldcomfort.com' ? '[email protected]' : '[email protected]' %>
See also
- The Using GnuPG to encrypt secrets recipe in this chapter
- https://docs.puppetlabs.com/guides/templating.html
modules/admin/templates/backup-mysql.sh.erb
with the following contents:#!/bin/sh /usr/bin/mysqldump -uroot \ -p<%= @mysql_password %> \ --all-databases | \ /bin/gzip > /backup/mysql/all-databases.sql.gz
site.pp
file as follows:node 'cookbook' { $mysql_password = 'secret' file { '/usr/local/bin/backup-mysql': content => template('admin/backup-mysql.sh.erb'), mode => '0755', } }
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412140971' Notice: /Stage[main]/Main/Node[cookbook]/File[/usr/local/bin/backup-mysql]/ensure: defined content as '{md5}c12af56559ef36529975d568ff52dca5' Notice: Finished catalog run in 0.31 seconds
[root@cookbook ~]# cat /usr/local/bin/backup-mysql #!/bin/sh /usr/bin/mysqldump -uroot \ -psecret \ --all-databases | \ /bin/gzip > /backup/mysql/all-databases.sql.gz
How it works…
Wherever a variable is referenced in the template, for example <%= @mysql_password %>
, Puppet will replace it with the corresponding value, secret
.
There's more…
In the example, we only used one variable in the template, but you can have as many as you like. These can also be facts:
ServerName <%= @fqdn %>
Or Ruby expressions:
MAILTO=<%= @emails.join(',') %>
Or any Ruby code you want:
ServerAdmin <%= @sitedomain == 'coldcomfort.com' ? '[email protected]' : '[email protected]' %>
See also
- The Using GnuPG to encrypt secrets recipe in this chapter
- https://docs.puppetlabs.com/guides/templating.html
referenced in the template, for example <%= @mysql_password %>
, Puppet will replace it with the corresponding value, secret
.
There's more…
In the example, we only used one variable in the template, but you can have as many as you like. These can also be facts:
ServerName <%= @fqdn %>
Or Ruby expressions:
MAILTO=<%= @emails.join(',') %>
Or any Ruby code you want:
ServerAdmin <%= @sitedomain == 'coldcomfort.com' ? '[email protected]' : '[email protected]' %>
See also
- The Using GnuPG to encrypt secrets recipe in this chapter
- https://docs.puppetlabs.com/guides/templating.html
See also
- The Using GnuPG to encrypt secrets recipe in this chapter
- https://docs.puppetlabs.com/guides/templating.html
Using array iteration in templates
In the previous example, we saw that you can use Ruby to interpolate different values in templates depending on the result of an expression. But you're not limited to getting one value at a time. You can put lots of them in a Puppet array and then have the template generate some content for each element of the array using a loop.
How to do it…
Follow these steps to build an example of iterating over arrays:
- Modify your
site.pp
file as follows:node 'cookbook' { $ipaddresses = ['192.168.0.1', '158.43.128.1', '10.0.75.207' ] file { '/tmp/addresslist.txt': content => template('base/addresslist.erb') } }
- Create the file
modules/base/templates/addresslist.erb
with the following contents:<% @ipaddresses.each do |ip| -%> IP address <%= ip %> is present <% end -%>
- Run Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412141917' Notice: /Stage[main]/Main/Node[cookbook]/File[/tmp/addresslist.txt]/ensure: defined content as '{md5}073851229d7b2843830024afb2b3902d' Notice: Finished catalog run in 0.30 seconds
- Check the contents of the generated file:
[root@cookbook ~]# cat /tmp/addresslist.txt IP address 192.168.0.1 is present. IP address 158.43.128.1 is present. IP address 10.0.75.207 is present.
How it works…
In the first line of the template, we reference the array ipaddresses
, and call its each
method:
<% @ipaddresses.each do |ip| -%>
In Ruby, this creates a loop that will execute once for each element of the array. Each time round the loop, the variable ip
will be set to the value of the current element.
In our example, the ipaddresses
array contains three elements, so the following line will be executed three times, once for each element:
IP address <%= ip %> is present.
This will result in three output lines:
IP address 192.168.0.1 is present.
IP address 158.43.128.1 is present.
IP address 10.0.75.207 is present.
The final line ends the loop:
<% end -%>
Note
Note that the first and last lines end with -%>
instead of just %>
as we saw before. The effect of the -
is to suppress the new line that would otherwise be generated on each pass through the loop, giving us unwanted blank lines in the file.
There's more…
Templates can also iterate over hashes, or arrays of hashes:
$interfaces = [ {name => 'eth0', ip => '192.168.0.1'},
{name => 'eth1', ip => '158.43.128.1'},
{name => 'eth2', ip => '10.0.75.207'} ]
<% @interfaces.each do |interface| -%>
Interface <%= interface['name'] %> has the address <%= interface['ip'] %>.
<% end -%>
Interface eth0 has the address 192.168.0.1.
Interface eth1 has the address 158.43.128.1.
Interface eth2 has the address 10.0.75.207.
See also
- The Using ERB templates recipe in this chapter
site.pp
file as follows:node 'cookbook' { $ipaddresses = ['192.168.0.1', '158.43.128.1', '10.0.75.207' ] file { '/tmp/addresslist.txt': content => template('base/addresslist.erb') } }
modules/base/templates/addresslist.erb
with the following contents:<% @ipaddresses.each do |ip| -%> IP address <%= ip %> is present <% end -%>
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412141917' Notice: /Stage[main]/Main/Node[cookbook]/File[/tmp/addresslist.txt]/ensure: defined content as '{md5}073851229d7b2843830024afb2b3902d' Notice: Finished catalog run in 0.30 seconds
[root@cookbook ~]# cat /tmp/addresslist.txt IP address 192.168.0.1 is present. IP address 158.43.128.1 is present. IP address 10.0.75.207 is present.
How it works…
In the first line of the template, we reference the array ipaddresses
, and call its each
method:
<% @ipaddresses.each do |ip| -%>
In Ruby, this creates a loop that will execute once for each element of the array. Each time round the loop, the variable ip
will be set to the value of the current element.
In our example, the ipaddresses
array contains three elements, so the following line will be executed three times, once for each element:
IP address <%= ip %> is present.
This will result in three output lines:
IP address 192.168.0.1 is present.
IP address 158.43.128.1 is present.
IP address 10.0.75.207 is present.
The final line ends the loop:
<% end -%>
Note
Note that the first and last lines end with -%>
instead of just %>
as we saw before. The effect of the -
is to suppress the new line that would otherwise be generated on each pass through the loop, giving us unwanted blank lines in the file.
There's more…
Templates can also iterate over hashes, or arrays of hashes:
$interfaces = [ {name => 'eth0', ip => '192.168.0.1'},
{name => 'eth1', ip => '158.43.128.1'},
{name => 'eth2', ip => '10.0.75.207'} ]
<% @interfaces.each do |interface| -%>
Interface <%= interface['name'] %> has the address <%= interface['ip'] %>.
<% end -%>
Interface eth0 has the address 192.168.0.1.
Interface eth1 has the address 158.43.128.1.
Interface eth2 has the address 10.0.75.207.
See also
- The Using ERB templates recipe in this chapter
template, we reference the array ipaddresses
, and call its each
method:
<% @ipaddresses.each do |ip| -%>
In Ruby, this creates a loop that will execute once for each element of the array. Each time round the loop, the variable ip
will be set to the value of the current element.
In our example, the ipaddresses
array contains three elements, so the following line will be executed three times, once for each element:
IP address <%= ip %> is present.
This will result in three output lines:
IP address 192.168.0.1 is present.
IP address 158.43.128.1 is present.
IP address 10.0.75.207 is present.
The final line ends the loop:
<% end -%>
Note
Note that the first and last lines end with -%>
instead of just %>
as we saw before. The effect of the -
is to suppress the new line that would otherwise be generated on each pass through the loop, giving us unwanted blank lines in the file.
There's more…
Templates can also iterate over hashes, or arrays of hashes:
$interfaces = [ {name => 'eth0', ip => '192.168.0.1'},
{name => 'eth1', ip => '158.43.128.1'},
{name => 'eth2', ip => '10.0.75.207'} ]
<% @interfaces.each do |interface| -%>
Interface <%= interface['name'] %> has the address <%= interface['ip'] %>.
<% end -%>
Interface eth0 has the address 192.168.0.1.
Interface eth1 has the address 158.43.128.1.
Interface eth2 has the address 10.0.75.207.
See also
- The Using ERB templates recipe in this chapter
over hashes, or arrays of hashes:
$interfaces = [ {name => 'eth0', ip => '192.168.0.1'},
{name => 'eth1', ip => '158.43.128.1'},
{name => 'eth2', ip => '10.0.75.207'} ]
<% @interfaces.each do |interface| -%>
Interface <%= interface['name'] %> has the address <%= interface['ip'] %>.
<% end -%>
Interface eth0 has the address 192.168.0.1.
Interface eth1 has the address 158.43.128.1.
Interface eth2 has the address 10.0.75.207.
See also
- The Using ERB templates recipe in this chapter
Using EPP templates
EPP templates are a new feature in Puppet 3.5 and newer versions. EPP templates use a syntax similar to ERB templates but are not compiled through Ruby. Two new functions are defined to call EPP templates, epp
, and inline_epp
. These functions are the EPP equivalents of the ERB functions template
and inline_template
, respectively. The main difference with EPP templates is that variables are referenced using the Puppet notation, $variable
instead of @variable
.
How to do it...
- Create an EPP template in
~/puppet/epp-test.epp
with the following content:This is <%= $message %>.
- Create an
epp.pp
manifest, which uses theepp
andinline_epp
functions:$message = "the message" file {'/tmp/epp-test': content => epp('/home/thomas/puppet/epp-test.epp') } notify {inline_epp('Also prints <%= $message %>'):}
- Apply the manifest making sure to use the future parser (the future parser is required for the
epp
andinline_epp
functions to be defined):t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future Notice: Compiled catalog for mylaptop in environment production in 1.03 seconds Notice: /Stage[main]/Main/File[/tmp/epp-test]/ensure: defined content as '{md5}999ccc2507d79d50fae0775d69b63b8c' Notice: Also prints the message
- Verify that the template worked as intended:
t@mylaptop ~/puppet $ cat /tmp/epp-test This is the message.
How it works...
Using the future parser, the epp
and inline_epp
functions are defined. The main difference between EPP templates and ERB templates is that variables are referenced in the same way they are within Puppet manifests.
There's more...
Both epp
and inline_epp
allow for variables to be overridden within the function call. A second parameter to the function call can be used to specify values for variables used within the scope of the function call. For example, we can override the value of $message
with the following code:
file {'/tmp/epp-test':
content => epp('/home/tuphill/puppet/epp-test.epp',
{ 'message' => "override $message"} )
}
notify {inline_epp('Also prints <%= $message %>',
{ 'message' => "inline override $message"}):}
Now when we run Puppet and verify the output we see that the value of $message
has been overridden:
t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future
Notice: Compiled catalog for mylaptop.pan.costco.com in environment production in 0.85 seconds
Notice: Also prints inline override the message
Notice: Finished catalog run in 0.05 seconds
t@mylaptop ~/puppet $ cat /tmp/epp-test
This is override the message.
~/puppet/epp-test.epp
with the following content:This is <%= $message %>.
epp.pp
manifest, which uses the epp
and inline_epp
functions:$message = "the message" file {'/tmp/epp-test': content => epp('/home/thomas/puppet/epp-test.epp') } notify {inline_epp('Also prints <%= $message %>'):}
epp
and inline_epp
functions to be defined):t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future Notice: Compiled catalog for mylaptop in environment production in 1.03 seconds Notice: /Stage[main]/Main/File[/tmp/epp-test]/ensure: defined content as '{md5}999ccc2507d79d50fae0775d69b63b8c' Notice: Also prints the message
t@mylaptop ~/puppet $ cat /tmp/epp-test This is the message.
How it works...
Using the future parser, the epp
and inline_epp
functions are defined. The main difference between EPP templates and ERB templates is that variables are referenced in the same way they are within Puppet manifests.
There's more...
Both epp
and inline_epp
allow for variables to be overridden within the function call. A second parameter to the function call can be used to specify values for variables used within the scope of the function call. For example, we can override the value of $message
with the following code:
file {'/tmp/epp-test':
content => epp('/home/tuphill/puppet/epp-test.epp',
{ 'message' => "override $message"} )
}
notify {inline_epp('Also prints <%= $message %>',
{ 'message' => "inline override $message"}):}
Now when we run Puppet and verify the output we see that the value of $message
has been overridden:
t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future
Notice: Compiled catalog for mylaptop.pan.costco.com in environment production in 0.85 seconds
Notice: Also prints inline override the message
Notice: Finished catalog run in 0.05 seconds
t@mylaptop ~/puppet $ cat /tmp/epp-test
This is override the message.
epp
and inline_epp
functions are defined. The main difference between EPP templates and ERB templates is that variables are referenced in the same way they are within Puppet manifests.
There's more...
Both epp
and inline_epp
allow for variables to be overridden within the function call. A second parameter to the function call can be used to specify values for variables used within the scope of the function call. For example, we can override the value of $message
with the following code:
file {'/tmp/epp-test':
content => epp('/home/tuphill/puppet/epp-test.epp',
{ 'message' => "override $message"} )
}
notify {inline_epp('Also prints <%= $message %>',
{ 'message' => "inline override $message"}):}
Now when we run Puppet and verify the output we see that the value of $message
has been overridden:
t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future
Notice: Compiled catalog for mylaptop.pan.costco.com in environment production in 0.85 seconds
Notice: Also prints inline override the message
Notice: Finished catalog run in 0.05 seconds
t@mylaptop ~/puppet $ cat /tmp/epp-test
This is override the message.
epp
and inline_epp
allow
for variables to be overridden within the function call. A second parameter to the function call can be used to specify values for variables used within the scope of the function call. For example, we can override the value of $message
with the following code:
file {'/tmp/epp-test':
content => epp('/home/tuphill/puppet/epp-test.epp',
{ 'message' => "override $message"} )
}
notify {inline_epp('Also prints <%= $message %>',
{ 'message' => "inline override $message"}):}
Now when we run Puppet and verify the output we see that the value of $message
has been overridden:
t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future
Notice: Compiled catalog for mylaptop.pan.costco.com in environment production in 0.85 seconds
Notice: Also prints inline override the message
Notice: Finished catalog run in 0.05 seconds
t@mylaptop ~/puppet $ cat /tmp/epp-test
This is override the message.
Using GnuPG to encrypt secrets
We often need Puppet to have access to secret information, such as passwords or crypto keys, for it to configure systems properly. But how do you avoid putting such secrets directly into your Puppet code, where they're visible to anyone who has read access to your repository?
It's a common requirement for third-party developers and contractors to be able to make changes via Puppet, but they definitely shouldn't see any confidential information. Similarly, if you're using a distributed Puppet setup like that described in Chapter 2, Puppet Infrastructure, every machine has a copy of the whole repo, including secrets for other machines that it doesn't need and shouldn't have. How can we prevent this?
One answer is to encrypt the secrets using the GnuPG tool, so that any secret information in the Puppet repo is undecipherable (for all practical purposes) without the appropriate key. Then we distribute the key securely to the people or machines that need it.
Getting ready
First you'll need an encryption key, so follow these steps to generate one. If you already have a GnuPG key that you'd like to use, go on to the next section. To complete this section, you will need to install the gpg command:
- Use
puppet
resource to install gpg:# puppet resource package gnupg ensure=installed
Tip
You may need to use gnupg2 as the package name, depending on your target OS.
- Run the following command. Answer the prompts as shown, except to substitute your name and e-mail address for mine. When prompted for a passphrase, just hit Enter:
t@mylaptop ~/puppet $ gpg --gen-key gpg (GnuPG) 1.4.18; Copyright (C) 2014 Free Software Foundation, Inc. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Please select what kind of key you want: (1) RSA and RSA (default) (2) DSA and Elgamal (3) DSA (sign only) (4) RSA (sign only) Your selection? 1 RSA keys may be between 1024 and 4096 bits long. What keysize do you want? (2048) 2048 Requested keysize is 2048 bits Please specify how long the key should be valid. 0 = key does not expire <n> = key expires in n days <n>w = key expires in n weeks <n>m = key expires in n months <n>y = key expires in n years Key is valid for? (0) 0 Key does not expire at all Is this correct? (y/N) y You need a user ID to identify your key; the software constructs the user ID from the Real Name, Comment and Email Address in this form: "Heinrich Heine (Der Dichter) <[email protected]>" Real name: Thomas Uphill Email address: [email protected] Comment: <enter> You selected this USER-ID: "Thomas Uphill <[email protected]>" Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o You need a Passphrase to protect your secret key.
Hit enter twice here to have an empty passphrase
You don't want a passphrase - this is probably a *bad* idea! I will do it anyway. You can change your passphrase at any time, using this program with the option "--edit-key". gpg: key F1C1EE49 marked as ultimately trusted public and secret key created and signed. gpg: checking the trustdb gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u pub 2048R/F1C1EE49 2014-10-01 Key fingerprint = 461A CB4C 397F 06A7 FB82 3BAD 63CF 50D8 F1C1 EE49 uid Thomas Uphill <[email protected]> sub 2048R/E2440023 2014-10-01
- You may see a message like this if your system is not configured with a source of randomness:
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy.
- In this case, install and start a random number generator daemon such as
haveged
orrng-tools
. Copy the gpg key you just created into thepuppet
user's account on your Puppet master:t@mylaptop ~ $ scp -r .gnupg [email protected]: gpg.conf 100% 7680 7.5KB/s 00:00 random_seed 100% 600 0.6KB/s 00:00 pubring.gpg 100% 1196 1.2KB/s 00:00 secring.gpg 100% 2498 2.4KB/s 00:00 trustdb.gpg 100% 1280 1.3KB/s 00:00
How to do it...
With your encryption key installed on the puppet
user's keyring (the key generation process described in the previous section will do this for you), you're ready to set up Puppet to decrypt secrets.
- Create the following directory:
t@cookbook:~/puppet$ mkdir -p modules/admin/lib/puppet/parser/functions
- Create the file
modules/admin/lib/puppet/parser/functions/secret.rb
with the following contents:module Puppet::Parser::Functions newfunction(:secret, :type => :rvalue) do |args| 'gpg --no-tty -d #{args[0]}' end end
- Create the file
secret_message
with the following contents:For a moment, nothing happened. Then, after a second or so, nothing continued to happen.
- Encrypt this file with the following command (use the e-mail address you supplied when creating the GnuPG key):
t@mylaptop ~/puppet $ gpg -e -r [email protected] secret_message
- Move the resulting encrypted file into your Puppet repo:
t@mylaptop:~/puppet$ mv secret_message.gpg modules/admin/files/
- Remove the original (plaintext) file:
t@mylaptop:~/puppet$ rm secret_message
- Modify your
site.pp
file as follows:node 'cookbook' { $message = secret('/etc/puppet/environments/production/ modules/admin/files/secret_message.gpg') notify { "The secret message is: ${message}": } }
- Run Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412145910' Notice: The secret message is: For a moment, nothing happened. Then, after a second or so, nothing continued to happen. Notice: Finished catalog run in 0.27 seconds
How it works...
First, we've created a custom function to allow Puppet to decrypt the secret files using GnuPG:
module Puppet::Parser::Functions
newfunction(:secret, :type => :rvalue) do |args|
'gpg --no-tty -d #{args[0]}'
end
end
The preceding code creates a function named secret
that takes a file path as an argument and returns the decrypted text. It doesn't manage encryption keys so you need to ensure that the puppet
user has the necessary key installed. You can check this with the following command:
puppet@puppet:~ $ gpg --list-secret-keys
/var/lib/puppet/.gnupg/secring.gpg
----------------------------------
sec 2048R/F1C1EE49 2014-10-01
uid Thomas Uphill <[email protected]>
ssb 2048R/E2440023 2014-10-01
Having set up the secret
function and the required key, we now encrypt a message to this key:
tuphill@mylaptop ~/puppet $ gpg -e -r [email protected] secret_message
This creates an encrypted file that can only be read by someone with access to the secret key (or Puppet running on a machine that has the secret key).
We then call the secret
function to decrypt this file and get the contents:
$message = secret(' /etc/puppet/environments/production/modules/admin/files/secret_message.gpg')
There's more...
You should use the secret
function, or something like it, to protect any confidential data in your Puppet repo: passwords, AWS credentials, license keys, even other secret keys such as SSL host keys.
You may decide to use a single key, which you push to machines as they're built, perhaps as part of a bootstrap process like that described in the Bootstrapping Puppet with Bash recipe in Chapter 2, Puppet Infrastructure. For even greater security, you might like to create a new key for each machine, or group of machines, and encrypt a given secret only for the machines that need it.
For example, your web servers might need a certain secret that you don't want to be accessible on any other machine. You could create a key for web servers, and encrypt the data only for this key.
If you want to use encrypted data with Hiera, there is a GnuPG backend for Hiera available at http://www.craigdunn.org/2011/10/secret-variables-in-puppet-with-hiera-and-gpg/.
See also
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
- The Storing secret data with hiera-gpg recipe in Chapter 2, Puppet Infrastructure
encryption key, so follow these steps to generate one. If you already have a GnuPG key that you'd like to use, go on to the next section. To complete this section, you will need to install the gpg command:
- Use
puppet
resource to install gpg:# puppet resource package gnupg ensure=installed
Tip
You may need to use gnupg2 as the package name, depending on your target OS.
- Run the following command. Answer the prompts as shown, except to substitute your name and e-mail address for mine. When prompted for a passphrase, just hit Enter:
t@mylaptop ~/puppet $ gpg --gen-key gpg (GnuPG) 1.4.18; Copyright (C) 2014 Free Software Foundation, Inc. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Please select what kind of key you want: (1) RSA and RSA (default) (2) DSA and Elgamal (3) DSA (sign only) (4) RSA (sign only) Your selection? 1 RSA keys may be between 1024 and 4096 bits long. What keysize do you want? (2048) 2048 Requested keysize is 2048 bits Please specify how long the key should be valid. 0 = key does not expire <n> = key expires in n days <n>w = key expires in n weeks <n>m = key expires in n months <n>y = key expires in n years Key is valid for? (0) 0 Key does not expire at all Is this correct? (y/N) y You need a user ID to identify your key; the software constructs the user ID from the Real Name, Comment and Email Address in this form: "Heinrich Heine (Der Dichter) <[email protected]>" Real name: Thomas Uphill Email address: [email protected] Comment: <enter> You selected this USER-ID: "Thomas Uphill <[email protected]>" Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o You need a Passphrase to protect your secret key.
Hit enter twice here to have an empty passphrase
You don't want a passphrase - this is probably a *bad* idea! I will do it anyway. You can change your passphrase at any time, using this program with the option "--edit-key". gpg: key F1C1EE49 marked as ultimately trusted public and secret key created and signed. gpg: checking the trustdb gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u pub 2048R/F1C1EE49 2014-10-01 Key fingerprint = 461A CB4C 397F 06A7 FB82 3BAD 63CF 50D8 F1C1 EE49 uid Thomas Uphill <[email protected]> sub 2048R/E2440023 2014-10-01
- You may see a message like this if your system is not configured with a source of randomness:
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy.
- In this case, install and start a random number generator daemon such as
haveged
orrng-tools
. Copy the gpg key you just created into thepuppet
user's account on your Puppet master:t@mylaptop ~ $ scp -r .gnupg [email protected]: gpg.conf 100% 7680 7.5KB/s 00:00 random_seed 100% 600 0.6KB/s 00:00 pubring.gpg 100% 1196 1.2KB/s 00:00 secring.gpg 100% 2498 2.4KB/s 00:00 trustdb.gpg 100% 1280 1.3KB/s 00:00
How to do it...
With your encryption key installed on the puppet
user's keyring (the key generation process described in the previous section will do this for you), you're ready to set up Puppet to decrypt secrets.
- Create the following directory:
t@cookbook:~/puppet$ mkdir -p modules/admin/lib/puppet/parser/functions
- Create the file
modules/admin/lib/puppet/parser/functions/secret.rb
with the following contents:module Puppet::Parser::Functions newfunction(:secret, :type => :rvalue) do |args| 'gpg --no-tty -d #{args[0]}' end end
- Create the file
secret_message
with the following contents:For a moment, nothing happened. Then, after a second or so, nothing continued to happen.
- Encrypt this file with the following command (use the e-mail address you supplied when creating the GnuPG key):
t@mylaptop ~/puppet $ gpg -e -r [email protected] secret_message
- Move the resulting encrypted file into your Puppet repo:
t@mylaptop:~/puppet$ mv secret_message.gpg modules/admin/files/
- Remove the original (plaintext) file:
t@mylaptop:~/puppet$ rm secret_message
- Modify your
site.pp
file as follows:node 'cookbook' { $message = secret('/etc/puppet/environments/production/ modules/admin/files/secret_message.gpg') notify { "The secret message is: ${message}": } }
- Run Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412145910' Notice: The secret message is: For a moment, nothing happened. Then, after a second or so, nothing continued to happen. Notice: Finished catalog run in 0.27 seconds
How it works...
First, we've created a custom function to allow Puppet to decrypt the secret files using GnuPG:
module Puppet::Parser::Functions
newfunction(:secret, :type => :rvalue) do |args|
'gpg --no-tty -d #{args[0]}'
end
end
The preceding code creates a function named secret
that takes a file path as an argument and returns the decrypted text. It doesn't manage encryption keys so you need to ensure that the puppet
user has the necessary key installed. You can check this with the following command:
puppet@puppet:~ $ gpg --list-secret-keys
/var/lib/puppet/.gnupg/secring.gpg
----------------------------------
sec 2048R/F1C1EE49 2014-10-01
uid Thomas Uphill <[email protected]>
ssb 2048R/E2440023 2014-10-01
Having set up the secret
function and the required key, we now encrypt a message to this key:
tuphill@mylaptop ~/puppet $ gpg -e -r [email protected] secret_message
This creates an encrypted file that can only be read by someone with access to the secret key (or Puppet running on a machine that has the secret key).
We then call the secret
function to decrypt this file and get the contents:
$message = secret(' /etc/puppet/environments/production/modules/admin/files/secret_message.gpg')
There's more...
You should use the secret
function, or something like it, to protect any confidential data in your Puppet repo: passwords, AWS credentials, license keys, even other secret keys such as SSL host keys.
You may decide to use a single key, which you push to machines as they're built, perhaps as part of a bootstrap process like that described in the Bootstrapping Puppet with Bash recipe in Chapter 2, Puppet Infrastructure. For even greater security, you might like to create a new key for each machine, or group of machines, and encrypt a given secret only for the machines that need it.
For example, your web servers might need a certain secret that you don't want to be accessible on any other machine. You could create a key for web servers, and encrypt the data only for this key.
If you want to use encrypted data with Hiera, there is a GnuPG backend for Hiera available at http://www.craigdunn.org/2011/10/secret-variables-in-puppet-with-hiera-and-gpg/.
See also
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
- The Storing secret data with hiera-gpg recipe in Chapter 2, Puppet Infrastructure
installed on the puppet
user's keyring (the key generation process described in the previous section will do this for you), you're ready to set up Puppet to decrypt secrets.
- Create the following directory:
t@cookbook:~/puppet$ mkdir -p modules/admin/lib/puppet/parser/functions
- Create the file
modules/admin/lib/puppet/parser/functions/secret.rb
with the following contents:module Puppet::Parser::Functions newfunction(:secret, :type => :rvalue) do |args| 'gpg --no-tty -d #{args[0]}' end end
- Create the file
secret_message
with the following contents:For a moment, nothing happened. Then, after a second or so, nothing continued to happen.
- Encrypt this file with the following command (use the e-mail address you supplied when creating the GnuPG key):
t@mylaptop ~/puppet $ gpg -e -r [email protected] secret_message
- Move the resulting encrypted file into your Puppet repo:
t@mylaptop:~/puppet$ mv secret_message.gpg modules/admin/files/
- Remove the original (plaintext) file:
t@mylaptop:~/puppet$ rm secret_message
- Modify your
site.pp
file as follows:node 'cookbook' { $message = secret('/etc/puppet/environments/production/ modules/admin/files/secret_message.gpg') notify { "The secret message is: ${message}": } }
- Run Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412145910' Notice: The secret message is: For a moment, nothing happened. Then, after a second or so, nothing continued to happen. Notice: Finished catalog run in 0.27 seconds
How it works...
First, we've created a custom function to allow Puppet to decrypt the secret files using GnuPG:
module Puppet::Parser::Functions
newfunction(:secret, :type => :rvalue) do |args|
'gpg --no-tty -d #{args[0]}'
end
end
The preceding code creates a function named secret
that takes a file path as an argument and returns the decrypted text. It doesn't manage encryption keys so you need to ensure that the puppet
user has the necessary key installed. You can check this with the following command:
puppet@puppet:~ $ gpg --list-secret-keys
/var/lib/puppet/.gnupg/secring.gpg
----------------------------------
sec 2048R/F1C1EE49 2014-10-01
uid Thomas Uphill <[email protected]>
ssb 2048R/E2440023 2014-10-01
Having set up the secret
function and the required key, we now encrypt a message to this key:
tuphill@mylaptop ~/puppet $ gpg -e -r [email protected] secret_message
This creates an encrypted file that can only be read by someone with access to the secret key (or Puppet running on a machine that has the secret key).
We then call the secret
function to decrypt this file and get the contents:
$message = secret(' /etc/puppet/environments/production/modules/admin/files/secret_message.gpg')
There's more...
You should use the secret
function, or something like it, to protect any confidential data in your Puppet repo: passwords, AWS credentials, license keys, even other secret keys such as SSL host keys.
You may decide to use a single key, which you push to machines as they're built, perhaps as part of a bootstrap process like that described in the Bootstrapping Puppet with Bash recipe in Chapter 2, Puppet Infrastructure. For even greater security, you might like to create a new key for each machine, or group of machines, and encrypt a given secret only for the machines that need it.
For example, your web servers might need a certain secret that you don't want to be accessible on any other machine. You could create a key for web servers, and encrypt the data only for this key.
If you want to use encrypted data with Hiera, there is a GnuPG backend for Hiera available at http://www.craigdunn.org/2011/10/secret-variables-in-puppet-with-hiera-and-gpg/.
See also
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
- The Storing secret data with hiera-gpg recipe in Chapter 2, Puppet Infrastructure
custom function to allow Puppet to decrypt the secret files using GnuPG:
module Puppet::Parser::Functions
newfunction(:secret, :type => :rvalue) do |args|
'gpg --no-tty -d #{args[0]}'
end
end
The preceding code creates a function named secret
that takes a file path as an argument and returns the decrypted text. It doesn't manage encryption keys so you need to ensure that the puppet
user has the necessary key installed. You can check this with the following command:
puppet@puppet:~ $ gpg --list-secret-keys
/var/lib/puppet/.gnupg/secring.gpg
----------------------------------
sec 2048R/F1C1EE49 2014-10-01
uid Thomas Uphill <[email protected]>
ssb 2048R/E2440023 2014-10-01
Having set up the secret
function and the required key, we now encrypt a message to this key:
tuphill@mylaptop ~/puppet $ gpg -e -r [email protected] secret_message
This creates an encrypted file that can only be read by someone with access to the secret key (or Puppet running on a machine that has the secret key).
We then call the secret
function to decrypt this file and get the contents:
$message = secret(' /etc/puppet/environments/production/modules/admin/files/secret_message.gpg')
There's more...
You should use the secret
function, or something like it, to protect any confidential data in your Puppet repo: passwords, AWS credentials, license keys, even other secret keys such as SSL host keys.
You may decide to use a single key, which you push to machines as they're built, perhaps as part of a bootstrap process like that described in the Bootstrapping Puppet with Bash recipe in Chapter 2, Puppet Infrastructure. For even greater security, you might like to create a new key for each machine, or group of machines, and encrypt a given secret only for the machines that need it.
For example, your web servers might need a certain secret that you don't want to be accessible on any other machine. You could create a key for web servers, and encrypt the data only for this key.
If you want to use encrypted data with Hiera, there is a GnuPG backend for Hiera available at http://www.craigdunn.org/2011/10/secret-variables-in-puppet-with-hiera-and-gpg/.
See also
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
- The Storing secret data with hiera-gpg recipe in Chapter 2, Puppet Infrastructure
secret
function, or something like it, to protect any confidential data in your Puppet repo: passwords, AWS credentials, license keys, even other secret keys such as SSL host keys.
You may decide to use a single key, which you push to machines as they're built, perhaps as part of a bootstrap process like that described in the Bootstrapping Puppet with Bash recipe in Chapter 2, Puppet Infrastructure. For even greater security, you might like to create a new key for each machine, or group of machines, and encrypt a given secret only for the machines that need it.
For example, your web servers might need a certain secret that you don't want to be accessible on any other machine. You could create a key for web servers, and encrypt the data only for this key.
If you want to use encrypted data with Hiera, there is a GnuPG backend for Hiera available at http://www.craigdunn.org/2011/10/secret-variables-in-puppet-with-hiera-and-gpg/.
See also
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
- The Storing secret data with hiera-gpg recipe in Chapter 2, Puppet Infrastructure
- Chapter 2, Puppet Infrastructure
- The Storing secret data with hiera-gpg recipe in Chapter 2, Puppet Infrastructure
Installing packages from a third-party repository
Most often you will want to install packages from the main distribution repo, so a simple package resource will do:
package { 'exim4': ensure => installed }
Sometimes, you need a package that is only found in a third-party repository (an Ubuntu PPA, for example), or it might be that you need a more recent version of a package than that provided by the distribution, which is available from a third party.
On a manually-administered machine, you would normally do this by adding the repo source configuration to /etc/apt/sources.list.d
(and, if necessary, a gpg key for the repo) before installing the package. We can automate this process easily with Puppet.
How to do it…
In this example, we'll use the popular Percona APT repo (Percona is a MySQL consulting firm who maintain and release their own specialized version of MySQL, more information is available at http://www.percona.com/software/repositories):
- Create the file
modules/admin/manifests/percona_repo.pp
with the following contents:# Install Percona APT repo class admin::percona_repo { exec { 'add-percona-apt-key': unless => '/usr/bin/apt-key list |grep percona', command => '/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -', notify => Exec['percona-apt-update'], } exec { 'percona-apt-update': command => '/usr/bin/apt-get update', require => [File['/etc/apt/sources.list.d/percona.list'], File['/etc/apt/preferences.d/00percona.pref']], refreshonly => true, } file { '/etc/apt/sources.list.d/percona.list': content => 'deb http://repo.percona.com/apt wheezy main', notify => Exec['percona-apt-update'], } file { '/etc/apt/preferences.d/00percona.pref': content => "Package: *\nPin: release o=Percona Development Team\nPin-Priority: 1001", notify => Exec['percona-apt-update'], } }
- Modify your
site.pp
file as follows:node 'cookbook' { include admin::percona_repo package { 'percona-server-server-5.5': ensure => installed, require => Class['admin::percona_repo'], } }
- Run Puppet:
root@cookbook-deb:~# puppet agent -t Info: Caching catalog for cookbook-deb Notice: /Stage[main]/Admin::Percona_repo/Exec[add-percona-apt-key]/returns: executed successfully Info: /Stage[main]/Admin::Percona_repo/Exec[add-percona-apt-key]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/File[/etc/apt/sources.list.d/percona.list]/ensure: defined content as '{md5}b8d479374497255804ffbf0a7bcdf6c2' Info: /Stage[main]/Admin::Percona_repo/File[/etc/apt/sources.list.d/percona.list]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/File[/etc/apt/preferences.d/00percona.pref]/ensure: defined content as '{md5}1d8ca6c1e752308a9bd3018713e2d1ad' Info: /Stage[main]/Admin::Percona_repo/File[/etc/apt/preferences.d/00percona.pref]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/Exec[percona-apt-update]: Triggered 'refresh' from 3 events
How it works…
In order to install any Percona package, we first need to have the repository configuration installed on the machine. This is why the percona-server-server-5.5
package (Percona's version of the standard MySQL server) requires the admin::percona_repo
class:
package { 'percona-server-server-5.5':
ensure => installed,
require => Class['admin::percona_repo'],
}
So, what does the admin::percona_repo
class do? It:
- Installs the Percona APT key with which the packages are signed
- Configures the Percona repo URL as a file in
/etc/apt/sources.list.d
- Runs
apt-get update
to retrieve the repo metadata - Adds an APT pin configuration in
/etc/apt/preferences.d
First of all, we install the APT key:
exec { 'add-percona-apt-key':
unless => '/usr/bin/apt-key list |grep percona',
command => '/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -',
notify => Exec['percona-apt-update'],
}
The unless
parameter checks the output of apt-key list
to make sure that the Percona key is not already installed, in which case we need not do anything. Assuming it isn't, the command
runs:
/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -
This command retrieves the key from the GnuPG keyserver, exports it in the ASCII format, and pipes this into the apt-key add
command, which adds it to the system keyring. You can use a similar pattern for most third-party repos that require an APT signing key.
Having installed the key, we add the repo configuration:
file { '/etc/apt/sources.list.d/percona.list':
content => 'deb http://repo.percona.com/apt wheezy main',
notify => Exec['percona-apt-update'],
}
Then run apt-get update
to update the system's APT cache with the metadata from the new repo:
exec { 'percona-apt-update':
command => '/usr/bin/apt-get update',
require => [File['/etc/apt/sources.list.d/percona.list'], File['/etc/apt/preferences.d/00percona.pref']],
refreshonly => true,
}
Finally, we configure the APT pin priority for the repo:
file { '/etc/apt/preferences.d/00percona.pref':
content => "Package: *\nPin: release o=Percona Development Team\nPin-Priority: 1001",
notify => Exec['percona-apt-update'],
}
This ensures that packages installed from the Percona repo will never be superseded by packages from somewhere else (the main Ubuntu distro, for example). Otherwise, you could end up with broken dependencies and be unable to install the Percona packages automatically.
There's more...
The APT package framework is specific to the Debian and Ubuntu systems. There is a forge module for managing apt repos, https://forge.puppetlabs.com/puppetlabs/apt. If you're on a Red Hat or CentOS-based system, you can use the yumrepo
resources to manage RPM repositories directly:
http://docs.puppetlabs.com/references/latest/type.html#yumrepo
the popular Percona APT repo (Percona is a MySQL consulting firm who maintain and release their own specialized version of MySQL, more information is available at http://www.percona.com/software/repositories):
- Create the file
modules/admin/manifests/percona_repo.pp
with the following contents:# Install Percona APT repo class admin::percona_repo { exec { 'add-percona-apt-key': unless => '/usr/bin/apt-key list |grep percona', command => '/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -', notify => Exec['percona-apt-update'], } exec { 'percona-apt-update': command => '/usr/bin/apt-get update', require => [File['/etc/apt/sources.list.d/percona.list'], File['/etc/apt/preferences.d/00percona.pref']], refreshonly => true, } file { '/etc/apt/sources.list.d/percona.list': content => 'deb http://repo.percona.com/apt wheezy main', notify => Exec['percona-apt-update'], } file { '/etc/apt/preferences.d/00percona.pref': content => "Package: *\nPin: release o=Percona Development Team\nPin-Priority: 1001", notify => Exec['percona-apt-update'], } }
- Modify your
site.pp
file as follows:node 'cookbook' { include admin::percona_repo package { 'percona-server-server-5.5': ensure => installed, require => Class['admin::percona_repo'], } }
- Run Puppet:
root@cookbook-deb:~# puppet agent -t Info: Caching catalog for cookbook-deb Notice: /Stage[main]/Admin::Percona_repo/Exec[add-percona-apt-key]/returns: executed successfully Info: /Stage[main]/Admin::Percona_repo/Exec[add-percona-apt-key]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/File[/etc/apt/sources.list.d/percona.list]/ensure: defined content as '{md5}b8d479374497255804ffbf0a7bcdf6c2' Info: /Stage[main]/Admin::Percona_repo/File[/etc/apt/sources.list.d/percona.list]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/File[/etc/apt/preferences.d/00percona.pref]/ensure: defined content as '{md5}1d8ca6c1e752308a9bd3018713e2d1ad' Info: /Stage[main]/Admin::Percona_repo/File[/etc/apt/preferences.d/00percona.pref]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/Exec[percona-apt-update]: Triggered 'refresh' from 3 events
How it works…
In order to install any Percona package, we first need to have the repository configuration installed on the machine. This is why the percona-server-server-5.5
package (Percona's version of the standard MySQL server) requires the admin::percona_repo
class:
package { 'percona-server-server-5.5':
ensure => installed,
require => Class['admin::percona_repo'],
}
So, what does the admin::percona_repo
class do? It:
- Installs the Percona APT key with which the packages are signed
- Configures the Percona repo URL as a file in
/etc/apt/sources.list.d
- Runs
apt-get update
to retrieve the repo metadata - Adds an APT pin configuration in
/etc/apt/preferences.d
First of all, we install the APT key:
exec { 'add-percona-apt-key':
unless => '/usr/bin/apt-key list |grep percona',
command => '/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -',
notify => Exec['percona-apt-update'],
}
The unless
parameter checks the output of apt-key list
to make sure that the Percona key is not already installed, in which case we need not do anything. Assuming it isn't, the command
runs:
/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -
This command retrieves the key from the GnuPG keyserver, exports it in the ASCII format, and pipes this into the apt-key add
command, which adds it to the system keyring. You can use a similar pattern for most third-party repos that require an APT signing key.
Having installed the key, we add the repo configuration:
file { '/etc/apt/sources.list.d/percona.list':
content => 'deb http://repo.percona.com/apt wheezy main',
notify => Exec['percona-apt-update'],
}
Then run apt-get update
to update the system's APT cache with the metadata from the new repo:
exec { 'percona-apt-update':
command => '/usr/bin/apt-get update',
require => [File['/etc/apt/sources.list.d/percona.list'], File['/etc/apt/preferences.d/00percona.pref']],
refreshonly => true,
}
Finally, we configure the APT pin priority for the repo:
file { '/etc/apt/preferences.d/00percona.pref':
content => "Package: *\nPin: release o=Percona Development Team\nPin-Priority: 1001",
notify => Exec['percona-apt-update'],
}
This ensures that packages installed from the Percona repo will never be superseded by packages from somewhere else (the main Ubuntu distro, for example). Otherwise, you could end up with broken dependencies and be unable to install the Percona packages automatically.
There's more...
The APT package framework is specific to the Debian and Ubuntu systems. There is a forge module for managing apt repos, https://forge.puppetlabs.com/puppetlabs/apt. If you're on a Red Hat or CentOS-based system, you can use the yumrepo
resources to manage RPM repositories directly:
http://docs.puppetlabs.com/references/latest/type.html#yumrepo
Percona package, we first need to have the repository configuration installed on the machine. This is why the percona-server-server-5.5
package (Percona's version of the standard MySQL server) requires the admin::percona_repo
class:
package { 'percona-server-server-5.5':
ensure => installed,
require => Class['admin::percona_repo'],
}
So, what does the admin::percona_repo
class do? It:
- Installs the Percona APT key with which the packages are signed
- Configures the Percona repo URL as a file in
/etc/apt/sources.list.d
- Runs
apt-get update
to retrieve the repo metadata - Adds an APT pin configuration in
/etc/apt/preferences.d
First of all, we install the APT key:
exec { 'add-percona-apt-key':
unless => '/usr/bin/apt-key list |grep percona',
command => '/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -',
notify => Exec['percona-apt-update'],
}
The unless
parameter checks the output of apt-key list
to make sure that the Percona key is not already installed, in which case we need not do anything. Assuming it isn't, the command
runs:
/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -
This command retrieves the key from the GnuPG keyserver, exports it in the ASCII format, and pipes this into the apt-key add
command, which adds it to the system keyring. You can use a similar pattern for most third-party repos that require an APT signing key.
Having installed the key, we add the repo configuration:
file { '/etc/apt/sources.list.d/percona.list':
content => 'deb http://repo.percona.com/apt wheezy main',
notify => Exec['percona-apt-update'],
}
Then run apt-get update
to update the system's APT cache with the metadata from the new repo:
exec { 'percona-apt-update':
command => '/usr/bin/apt-get update',
require => [File['/etc/apt/sources.list.d/percona.list'], File['/etc/apt/preferences.d/00percona.pref']],
refreshonly => true,
}
Finally, we configure the APT pin priority for the repo:
file { '/etc/apt/preferences.d/00percona.pref':
content => "Package: *\nPin: release o=Percona Development Team\nPin-Priority: 1001",
notify => Exec['percona-apt-update'],
}
This ensures that packages installed from the Percona repo will never be superseded by packages from somewhere else (the main Ubuntu distro, for example). Otherwise, you could end up with broken dependencies and be unable to install the Percona packages automatically.
There's more...
The APT package framework is specific to the Debian and Ubuntu systems. There is a forge module for managing apt repos, https://forge.puppetlabs.com/puppetlabs/apt. If you're on a Red Hat or CentOS-based system, you can use the yumrepo
resources to manage RPM repositories directly:
http://docs.puppetlabs.com/references/latest/type.html#yumrepo
for managing apt repos, https://forge.puppetlabs.com/puppetlabs/apt. If you're on a Red Hat or CentOS-based system, you can use the yumrepo
resources to manage RPM repositories directly:
http://docs.puppetlabs.com/references/latest/type.html#yumrepo
Comparing package versions
Package version numbers are odd things. They look like decimal numbers, but they're not: a version number is often in the form of 2.6.4
, for example. If you need to compare one version number with another, you can't do a straightforward string comparison: 2.6.4
would be interpreted as greater than 2.6.12
. And a numeric comparison won't work because they're not valid numbers.
Puppet's versioncmp
function comes to the rescue. If you pass two things that look like version numbers, it will compare them and return a value indicating which is greater:
versioncmp( A, B )
returns:
- 0 if A and B are equal
- Greater than 1 if A is higher than B
- Less than 0 if A is less than B
How to do it…
Here's an example using the versioncmp
function:
- Modify your
site.pp
file as follows:node 'cookbook' { $app_version = '1.2.2' $min_version = '1.2.10' if versioncmp($app_version, $min_version) >= 0 { notify { 'Version OK': } } else { notify { 'Upgrade needed': } } }
- Run Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Notice: Upgrade needed
- Now change the value of
$app_version
:$app_version = '1.2.14'
- Run Puppet again:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Notice: Version OK
How it works…
We've specified that the minimum acceptable version ($min_version
) is 1.2.10
. So, in the first example, we want to compare it with $app_version
of 1.2.2
. A simple alphabetic comparison of these two strings (in Ruby, for example) would give the wrong result, but versioncmp
correctly determines that 1.2.2
is less than 1.2.10
and alerts us that we need to upgrade.
In the second example, $app_version
is now 1.2.14
, which versioncmp
correctly recognizes as greater than $min_version
and so we get the message Version OK.
versioncmp
function:
site.pp
file as follows:node 'cookbook' { $app_version = '1.2.2' $min_version = '1.2.10' if versioncmp($app_version, $min_version) >= 0 { notify { 'Version OK': } } else { notify { 'Upgrade needed': } } }
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Notice: Upgrade needed
$app_version
:$app_version = '1.2.14'
- Puppet again:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Notice: Version OK
How it works…
We've specified that the minimum acceptable version ($min_version
) is 1.2.10
. So, in the first example, we want to compare it with $app_version
of 1.2.2
. A simple alphabetic comparison of these two strings (in Ruby, for example) would give the wrong result, but versioncmp
correctly determines that 1.2.2
is less than 1.2.10
and alerts us that we need to upgrade.
In the second example, $app_version
is now 1.2.14
, which versioncmp
correctly recognizes as greater than $min_version
and so we get the message Version OK.
$min_version
) is 1.2.10
. So, in the first example, we want to compare it with $app_version
of 1.2.2
. A simple alphabetic comparison of these two strings (in Ruby, for example) would give the wrong result, but versioncmp
correctly determines that 1.2.2
is less than 1.2.10
and alerts us that we need to upgrade.
$app_version
is now 1.2.14
, which versioncmp
correctly recognizes as greater than $min_version
and so we get the message Version OK.