Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
DevOps: Puppet, Docker, and Kubernetes

You're reading from   DevOps: Puppet, Docker, and Kubernetes Practical recipes to make the most of DevOps with powerful tools

Arrow left icon
Product type Course
Published in Mar 2017
Publisher Packt
ISBN-13 9781788297615
Length 925 pages
Edition 1st Edition
Tools
Concepts
Arrow right icon
Authors (6):
Arrow left icon
Ke-Jou Carol Hsu Ke-Jou Carol Hsu
Author Profile Icon Ke-Jou Carol Hsu
Ke-Jou Carol Hsu
Neependra Khare Neependra Khare
Author Profile Icon Neependra Khare
Neependra Khare
John Arundel John Arundel
Author Profile Icon John Arundel
John Arundel
Hideto Saito Hideto Saito
Author Profile Icon Hideto Saito
Hideto Saito
Thomas Uphill Thomas Uphill
Author Profile Icon Thomas Uphill
Thomas Uphill
Hui-Chuan Chloe Lee Hui-Chuan Chloe Lee
Author Profile Icon Hui-Chuan Chloe Lee
Hui-Chuan Chloe Lee
+2 more Show less
Arrow right icon
View More author details
Toc

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.

  1. Create a manifest named 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'],
      }
  2. Run 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
    
  3. 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.

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.

  1. Create a manifest named 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'],
      }
  2. Run 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
    
  3. 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.

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 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'],
  }
Run 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
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.

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.

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.

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:

  1. Create an initest.pp manifest with the following contents:
      ini_setting {'server_true':
        path    => '/tmp/server.conf',
        section => 'main',
        setting => 'server',
        value   => 'true',
      }
  2. 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
    
  3. 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.

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:

  1. Create an initest.pp manifest with the following contents:
      ini_setting {'server_true':
        path    => '/tmp/server.conf',
        section => 'main',
        setting => 'server',
        value   => 'true',
      }
  2. 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
    
  3. 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.

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.

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.

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.

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:

  1. 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'],
        }
      }
  2. 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
    
  3. 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.

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.

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.

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.

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.

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.

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.

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.

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:

  1. 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
  2. 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',
      }
    }
  3. 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
    
  4. 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' ? 'seth@coldcomfort.com' : 'flora@poste.com' %>

See also

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' ? 'seth@coldcomfort.com' : 'flora@poste.com' %>

See also

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' ? 'seth@coldcomfort.com' : 'flora@poste.com' %>

See also

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' ? 'seth@coldcomfort.com' : 'flora@poste.com' %>

See also

See also

The Using GnuPG to encrypt secrets recipe in this chapter

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:

  1. 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')
        }
      }
  2. Create the file modules/base/templates/addresslist.erb with the following contents:
    <% @ipaddresses.each do |ip| -%>
    IP address <%= ip %> is present
    <% end -%>
  3. 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
    
  4. 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
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
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
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
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...

  1. Create an EPP template in ~/puppet/epp-test.epp with the following content:
    This is <%= $message %>.
  2. Create an 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 %>'):}
  3. Apply the manifest making sure to use the future parser (the future parser is required for the 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
    
  4. 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.
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 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 %>'):}
Apply the manifest making sure to use the future parser (the future parser is required for the 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
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.
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.
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.

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:

  1. 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.

  2. 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) <heinrichh@duesseldorf.de>"
    
    Real name: Thomas Uphill
    Email address: thomas@narrabilis.com
    Comment: <enter>
    You selected this USER-ID:
        "Thomas Uphill <thomas@narrabilis.com>"
    
    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 <thomas@narrabilis.com>
    sub   2048R/E2440023 2014-10-01
    
  3. 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.
    
  4. In this case, install and start a random number generator daemon such as haveged or rng-tools. Copy the gpg key you just created into the puppet user's account on your Puppet master:
    t@mylaptop ~ $ scp -r .gnupg puppet@puppet.example.com:
    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.

  1. Create the following directory:
    t@cookbook:~/puppet$ mkdir -p modules/admin/lib/puppet/parser/functions
    
  2. 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
  3. Create the file secret_message with the following contents:
    For a moment, nothing happened.
    Then, after a second or so, nothing continued to happen.
  4. 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 thomas@narrabilis.com secret_message
    
  5. Move the resulting encrypted file into your Puppet repo:
    t@mylaptop:~/puppet$ mv secret_message.gpg modules/admin/files/
    
  6. Remove the original (plaintext) file:
    t@mylaptop:~/puppet$ rm secret_message
    
  7. 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}": }
    }
  8. 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 <thomas@narrabilis.com>
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 thomas@narrabilis.com 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
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:

  1. 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.

  2. 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) <heinrichh@duesseldorf.de>"
    
    Real name: Thomas Uphill
    Email address: thomas@narrabilis.com
    Comment: <enter>
    You selected this USER-ID:
        "Thomas Uphill <thomas@narrabilis.com>"
    
    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 <thomas@narrabilis.com>
    sub   2048R/E2440023 2014-10-01
    
  3. 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.
    
  4. In this case, install and start a random number generator daemon such as haveged or rng-tools. Copy the gpg key you just created into the puppet user's account on your Puppet master:
    t@mylaptop ~ $ scp -r .gnupg puppet@puppet.example.com:
    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.

  1. Create the following directory:
    t@cookbook:~/puppet$ mkdir -p modules/admin/lib/puppet/parser/functions
    
  2. 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
  3. Create the file secret_message with the following contents:
    For a moment, nothing happened.
    Then, after a second or so, nothing continued to happen.
  4. 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 thomas@narrabilis.com secret_message
    
  5. Move the resulting encrypted file into your Puppet repo:
    t@mylaptop:~/puppet$ mv secret_message.gpg modules/admin/files/
    
  6. Remove the original (plaintext) file:
    t@mylaptop:~/puppet$ rm secret_message
    
  7. 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}": }
    }
  8. 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 <thomas@narrabilis.com>
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 thomas@narrabilis.com 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
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.

  1. Create the following directory:
    t@cookbook:~/puppet$ mkdir -p modules/admin/lib/puppet/parser/functions
    
  2. 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
  3. Create the file secret_message with the following contents:
    For a moment, nothing happened.
    Then, after a second or so, nothing continued to happen.
  4. 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 thomas@narrabilis.com secret_message
    
  5. Move the resulting encrypted file into your Puppet repo:
    t@mylaptop:~/puppet$ mv secret_message.gpg modules/admin/files/
    
  6. Remove the original (plaintext) file:
    t@mylaptop:~/puppet$ rm secret_message
    
  7. 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}": }
    }
  8. 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 <thomas@narrabilis.com>
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 thomas@narrabilis.com 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
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 <thomas@narrabilis.com>
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 thomas@narrabilis.com 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
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
See also

The Configuring Hiera recipe in
  • 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):

  1. 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'],
      }
    }
  2. 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'],
      }
    }
  3. 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

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):

  1. 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'],
      }
    }
  2. 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'],
      }
    }
  3. 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

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

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

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:

  1. 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': }
      }
    }
  2. Run Puppet:
    [root@cookbook ~]# puppet agent -t
    Info: Caching catalog for cookbook.example.com
    Notice: Upgrade needed
    
  3. Now change the value of $app_version:
    $app_version = '1.2.14'
  4. 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.

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
  1. 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.

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.

You have been reading a chapter from
DevOps: Puppet, Docker, and Kubernetes
Published in: Mar 2017
Publisher: Packt
ISBN-13: 9781788297615
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime