Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Puppet Language and Style

Save for later
  • 1080 min read
  • 2015-02-20 00:00:00

article-image

In this article by Thomas Uphill, author of the book Puppet Cookbook Third Edition, we will cover the following recipes:

  • Installing a package before starting a service
  • Installing, configuring, and starting a service
  • Using regular expressions in if statements
  • Using selectors and case statements
  • Creating a centralized Puppet infrastructure
  • Creating certificates with multiple DNS names

(For more resources related to this topic, see here.)

Installing a package before starting a service

To show how ordering works, we'll create a manifest that installs httpd and then ensures the httpd package service is running.

How to do it...

  1. We start by creating a manifest that defines the service:
    service {'httpd':
       ensure => running,
       require => Package['httpd'],
    }
  2. The service definition references a package resource named httpd; we now need to define that resource:
    package {'httpd':
       ensure => 'installed',
    }

How it works...

In this example, the package will be installed before the service is started. Using require within the definition of the httpd service ensures that the package is installed first, regardless of the order within the manifest file.

Capitalization

Capitalization is important in Puppet. In our previous example, we created a package named httpd. If we wanted to refer to this package later, we would capitalize its type (package) as follows:

Package['httpd']

To refer to a class, for example, the something::somewhere class, which has already been included/defined in your manifest, you can reference it with the full path as follows:

Class['something::somewhere']

When you have a defined type, for example the following defined type:

example::thing {'one':}

The preceding resource may be referenced later as follows:

Example::Thing['one']

Knowing how to reference previously defined resources is necessary for the next section on metaparameters and ordering.

Learning metaparameters and ordering

All the manifests that will be used to define a node are compiled into a catalog. A catalog is the code that will be applied to configure a node. It is important to remember that manifests are not applied to nodes sequentially. There is no inherent order to the application of manifests. With this in mind, in the previous httpd example, what if we wanted to ensure that the httpd process started after the httpd package was installed?

We couldn't rely on the httpd service coming after the httpd package in the manifests. What we have to do is use metaparameters to tell Puppet the order in which we want resources applied to the node. Metaparameters are parameters that can be applied to any resource and are not specific to any one resource type. They are used for catalog compilation and as hints to Puppet but not to define anything about the resource to which they are attached. When dealing with ordering, there are four metaparameters used:

  • before
  • require
  • notify
  • subscribe

The before and require metaparameters specify a direct ordering; notify implies before and subscribe implies require. The notify metaparameter is only applicable to services; what notify does is tell a service to restart after the notifying resource has been applied to the node (this is most often a package or file resource). In the case of files, once the file is created on the node, a notify parameter will restart any services mentioned. The subscribe metaparameter has the same effect but is defined on the service; the service will subscribe to the file.

Trifecta

The relationship between package and service previously mentioned is an important and powerful paradigm of Puppet. Adding one more resource-type file into the fold, creates what puppeteers refer to as the trifecta. Almost all system administration tasks revolve around these three resource types. As a system administrator, you install a package, configure the package with files, and then start the service.

puppet-language-and-style-img-0

Diagram of Trifecta (Files require package for directory, service requires files and package)

Idempotency

A key concept of Puppet is that the state of the system when a catalog is applied to a node cannot affect the outcome of Puppet run. In other words, at the end of Puppet run (if the run was successful), the system will be in a known state and any further application of the catalog will result in a system that is in the same state. This property of Puppet is known as idempotency. Idempotency is the property that no matter how many times you do something, it remains in the same state as the first time you did it. For instance, if you had a light switch and you gave the instruction to turn it on, the light would turn on. If you gave the instruction again, the light would remain on.

Installing, configuring, and starting a service

There are many examples of this pattern online. In our simple example, we will create an Apache configuration file under /etc/httpd/conf.d/cookbook.conf. The /etc/httpd/conf.d directory will not exist until the httpd package is installed. After this file is created, we would want httpd to restart to notice the change; we can achieve this with a notify parameter.

How to do it...

We will need the same definitions as our last example; we need the package and service installed. We now need two more things. We need the configuration file and index page (index.html) created. For this, we follow these steps:

  1. As in the previous example, we ensure the service is running and specify that the service requires the httpd package:
    service {'httpd':
       ensure => running,
       require => Package['httpd'],
    }
  2. We then define the package as follows:
    package {'httpd':
       ensure => installed,
    }
  3. Now, we create the /etc/httpd/conf.d/cookbook.conf configuration file; the /etc/httpd/conf.d directory will not exist until the httpd package is installed. The require metaparameter tells Puppet that this file requires the httpd package to be installed before it is created:
    file {'/etc/httpd/conf.d/cookbook.conf':
       content => "<VirtualHost *:80>nServername     cookbooknDocumentRoot     
    /var/www/cookbookn</VirtualHost>n",    require => Package['httpd'],    notify => Service['httpd'], }
  4. We then go on to create an index.html file for our virtual host in /var/www/cookbook. This directory won't exist yet, so we need to create this as well, using the following code:
    file {'/var/www/cookbook':
       ensure => directory,
    }
    file {'/var/www/cookbook/index.html':
       content => "<html><h1>Hello World!</h1></html>n",
       require => File['/var/www/cookbook'],
    }

How it works…

The require attribute to the file resources tell Puppet that we need the /var/www/cookbook directory created before we can create the index.html file. The important concept to remember is that we cannot assume anything about the target system (node). We need to define everything on which the target depends. Anytime you create a file in a manifest, you have to ensure that the directory containing that file exists. Anytime you specify that a service should be running, you have to ensure that the package providing that service is installed.

In this example, using metaparameters, we can be confident that no matter what state the node is in before running Puppet, after Puppet runs, the following will be true:

  • httpd will be running
  • The VirtualHost configuration file will exist
  • httpd will restart and be aware of the VirtualHost file
  • The DocumentRoot directory will exist
  • An index.html file will exist in the DocumentRoot directory

Using regular expressions in if statements

Another kind of expression you can test in if statements and other conditionals is the regular expression. A regular expression is a powerful way to compare strings using pattern matching.

How to do it…

This is one example of using a regular expression in a conditional statement. Add the following to your manifest:

if $::architecture =~ /64/ {
notify { '64Bit OS Installed': }
} else {
notify { 'Upgrade to 64Bit': }
fail('Not 64 Bit')
}

How it works…

Puppet treats the text supplied between the forward slashes as a regular expression, specifying the text to be matched. If the match succeeds, the if expression will be true and so the code between the first set of curly braces will be executed. In this example, we used a regular expression because different distributions have different ideas on what to call 64bit; some use amd64, while others use x86_64. The only thing we can count on is the presence of the number 64 within the fact. Some facts that have version numbers in them are treated as strings to Puppet. For instance, $::facterversion. On my test system, this is 2.0.1, but when I try to compare that with 2, Puppet fails to make the comparison:

Error: comparison of String with 2 failed at /home/thomas/.puppet/manifests/version.pp:1 on node cookbook.example.com

If you wanted instead to do something if the text does not match, use !~ rather than =~:

if $::kernel !~ /Linux/ {
notify { 'Not Linux, could be Windows, MacOS X, AIX, or ?': }
}

There's more…

Regular expressions are very powerful, but can be difficult to understand and debug. If you find yourself using a regular expression so complex that you can't see at a glance what it does, think about simplifying your design to make it easier. However, one particularly useful feature of regular expressions is the ability to capture patterns.

Capturing patterns

You can not only match text using a regular expression, but also capture the matched text and store it in a variable:

$input = 'Puppet is better than manual configuration'
if $input =~ /(.*) is better than (.*)/ {
notify { "You said '${0}'. Looks like you're comparing ${1}
   to ${2}!": }
}

The preceding code produces this output:

You said 'Puppet is better than manual configuration'. Looks like you're comparing Puppet to manual configuration!

The variable $0 stores the whole matched text (assuming the overall match succeeded). If you put brackets around any part of the regular expression, it creates a group, and any matched groups will also be stored in variables. The first matched group will be $1, the second $2, and so on, as shown in the preceding example.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $19.99/month. Cancel anytime

Regular expression syntax

Puppet's regular expression syntax is the same as Ruby's, so resources that explain Ruby's regular expression syntax will also help you with Puppet. You can find a good introduction to Ruby's regular expression syntax at this website:

http://www.tutorialspoint.com/ruby/ruby_regular_expressions.htm.

Using selectors and case statements

Although you could write any conditional statement using if, Puppet provides a couple of extra forms to help you express conditionals more easily: the selector and the case statement.

How to do it…

Here are some examples of selector and case statements:

  1. Add the following code to your manifest:
    $systemtype = $::operatingsystem ? {
    'Ubuntu' => 'debianlike',
    'Debian' => 'debianlike',
    'RedHat' => 'redhatlike',
    'Fedora' => 'redhatlike',
    'CentOS' => 'redhatlike',
    default => 'unknown',
    }
     
    notify { "You have a ${systemtype} system": }
  2. Add the following code to your manifest:
    class debianlike {
    notify { 'Special manifest for Debian-like systems': }
    }
     
    class redhatlike {
    notify { 'Special manifest for RedHat-like systems': }
    }
     
    case $::operatingsystem {
    'Ubuntu',
    'Debian': {
       include debianlike
    }
    'RedHat',
    'Fedora',
    'CentOS',
    'Springdale': {
       include redhatlike
    }
    default: {
       notify { "I don't know what kind of system you have!":
       }
    }
    }

How it works…

Our example demonstrates both the selector and the case statement, so let's see in detail how each of them works.

Selector

In the first example, we used a selector (the ? operator) to choose a value for the $systemtype variable depending on the value of $::operatingsystem. This is similar to the ternary operator in C or Ruby, but instead of choosing between two possible values, you can have as many values as you like.

Puppet will compare the value of $::operatingsystem to each of the possible values we have supplied in Ubuntu, Debian, and so on. These values could be regular expressions (for example, for a partial string match, or to use wildcards), but in our case, we have just used literal strings.

As soon as it finds a match, the selector expression returns whatever value is associated with the matching string. If the value of $::operatingsystem is Fedora, for example, the selector expression will return the redhatlike string and this will be assigned to the variable $systemtype.

Case statement

Unlike selectors, the case statement does not return a value. case statements come in handy when you want to execute different code depending on the value of some expression. In our second example, we used the case statement to include either the debianlike or redhatlike class, depending on the value of $::operatingsystem.

Again, Puppet compares the value of $::operatingsystem to a list of potential matches. These could be regular expressions or strings, or as in our example, comma-separated lists of strings. When it finds a match, the associated code between curly braces is executed. So, if the value of $::operatingsystem is Ubuntu, then the code including debianlike will be executed.

There's more…

Once you've got a grip of the basic use of selectors and case statements, you may find the following tips useful.

Regular expressions

As with if statements, you can use regular expressions with selectors and case statements, and you can also capture the values of the matched groups and refer to them using $1, $2, and so on:

case $::lsbdistdescription {
/Ubuntu (.+)/: {
   notify { "You have Ubuntu version ${1}": }
}
/CentOS (.+)/: {
   notify { "You have CentOS version ${1}": }
}
default: {}
}

Defaults

Both selectors and case statements let you specify a default value, which is chosen if none of the other options match (the style guide suggests you always have a default clause defined):

$lunch = 'Filet mignon.'
$lunchtype = $lunch ? {
/fries/ => 'unhealthy',
/salad/ => 'healthy',
default => 'unknown',
}
 
notify { "Your lunch was ${lunchtype}": }

The output is as follows:

t@mylaptop ~ $ puppet apply lunchtype.pp
Notice: Your lunch was unknown
Notice: /Stage[main]/Main/Notify[Your lunch was unknown]
/message: defined 'message' as 'Your lunch was unknown'

When the default action shouldn't normally occur, use the fail() function to halt the Puppet run.

How to do it…

The following steps will show you how to use the in operator:

  1. Add the following code to your manifest:
    if $::operatingsystem in [ 'Ubuntu', 'Debian' ] {
    notify { 'Debian-type operating system detected': }
    } elsif $::operatingsystem in [ 'RedHat', 'Fedora', 'SuSE',   'CentOS' ] {
    notify { 'RedHat-type operating system detected': }
    } else {
    notify { 'Some other operating system detected': }
    }
  2. Run Puppet:
    t@cookbook:~/.puppet/manifests$ puppet apply in.pp
    Notice: Compiled catalog for cookbook.example.com in environment production in 0.03 seconds
    Notice: Debian-type operating system detected
    Notice: /Stage[main]/Main/Notify[Debian-type operating system 
    detected]/message: defined 'message' as 'Debian-type operating system detected'
    Notice: Finished catalog run in 0.02 seconds

There's more…

The value of an in expression is Boolean (true or false) so you can assign it to a variable:

$debianlike = $::operatingsystem in [ 'Debian', 'Ubuntu' ]
 
if $debianlike {
notify { 'You are in a maze of twisty little packages, all alike': }
}

Creating a centralized Puppet infrastructure

A configuration management tool such as Puppet is best used when you have many machines to manage. If all the machines can reach a central location, using a centralized Puppet infrastructure might be a good solution. Unfortunately, Puppet doesn't scale well with a large number of nodes. If your deployment has less than 800 servers, a single Puppet master should be able to handle the load, assuming your catalogs are not complex (take less than 10 seconds to compile each catalog). If you have a larger number of nodes, I suggest a load balancing configuration described in Mastering Puppet, Thomas Uphill, Packt Publishing.

A Puppet master is a Puppet server that acts as an X509 certificate authority for Puppet and distributes catalogs (compiled manifests) to client nodes. Puppet ships with a built-in web server called WEBrick, which can handle a very small number of nodes. In this section, we will see how to use that built-in server to control a very small (less than 10) number of nodes.

Getting ready

The Puppet master process is started by running puppet master; most Linux distributions have start and stop scripts for the Puppet master in a separate package. To get started, we'll create a new debian server named puppet.example.com.

How to do it...

  1. Install Puppet on the new server and then use Puppet to install the Puppet master package:
    # puppet resource package puppetmaster ensure='installed' Notice: /Package[puppetmaster]/ensure: 
    created package { 'puppetmaster':   ensure => '3.7.0-1puppetlabs1', }
  2. Now start the Puppet master service and ensure it will start at boot:
    # puppet resource service puppetmaster ensure=true enable=true service 
    { 'puppetmaster':   ensure => 'running',   enable => 'true', }

How it works...

The Puppet master package includes the start and stop scripts for the Puppet master service. We use Puppet to install the package and start the service. Once the service is started, we can point another node at the Puppet master (you might need to disable the host-based firewall on your machine).

  1. From another node, run puppet agent to start a puppet agent, which will contact the server and request a new certificate:
    t@ckbk:~$ sudo puppet agent -t
    Info: Creating a new SSL key for cookbook.example.com
    Info: Caching certificate for ca
    Info: Creating a new SSL certificate request for cookbook.example.com
    Info: Certificate Request fingerprint (SHA256): 
    06:C6:2B:C4:97:5D:16:F2:73:82:C4:A9:A7:B1:D0:95:AC:69:7B:27:13:A9:1A:4C:98:20:21:C2:50:48:66:A2 Info: Caching certificate for ca Exiting; no certificate found and waitforcert is disabled
  2. Now on the Puppet server, sign the new key:
    root@puppet:~# puppet cert list
    pu "cookbook.example.com" (SHA256) 
    06:C6:2B:C4:97:5D:16:F2:73:82:C4:A9:A7:B1:D0:95:AC:69:7B:27:13:A9:1A:4C:98:20:21:C2:50:48:66:A2 root@puppet:~# puppet cert sign cookbook.example.com Notice: Signed certificate request for cookbook.example.com Notice: Removing file Puppet::SSL::CertificateRequestcookbook.example.com
    at'/var/lib/puppet/ssl/ca/requests/cookbook.example.com.pem'
  3. Return to the cookbook node and run Puppet again:
    t@ckbk:~$ sudo puppet agent –vt
    Info: Caching certificate for cookbook.example.com
    Info: Caching certificate_revocation_list for ca
    Info: Caching certificate for cookbook.example.comInfo: Retrieving pluginfactsInfo:
    Retrieving pluginInfo: Caching catalog for cookbookInfo:
    Applying configuration version '1410401823'Notice: Finished catalog run in 0.04 seconds

There's more...

When we ran puppet agent, Puppet looked for a host named puppet.example.com (since our test node is in the example.com domain); if it couldn't find that host, it would then look for a host named Puppet. We can specify the server to contact with the --server option to puppet agent. When we installed the Puppet master package and started the Puppet master service, Puppet created default SSL certificates based on our hostname.

Creating certificates with multiple DNS names

By default, Puppet will create an SSL certificate for your Puppet master that contains the fully qualified domain name of the server only. Depending on how your network is configured, it can be useful for the server to be known by other names. In this recipe, we'll make a new certificate for our Puppet master that has multiple DNS names.

Getting ready

Install the Puppet master package if you haven't already done so. You will then need to start the Puppet master service at least once to create a certificate authority (CA).

How to do it...

The steps are as follows:

  1. Stop the running Puppet master process with the following command:
    # service puppetmaster stop
    [ ok ] Stopping puppet master.
  2. Delete (clean) the current server certificate:
    # puppet cert clean puppet
    Notice: Revoked certificate with serial 6
    Notice: Removing file Puppet::SSL::Certificate puppet at '/var/lib/puppet/ssl/ca/signed/puppet.pem'
    Notice: Removing file Puppet::SSL::Certificate puppet at '/var/lib/puppet/ssl/certs/puppet.pem'
    Notice: Removing file Puppet::SSL::Key puppet at '/var/lib/puppet/ssl/private_keys/puppet.pem'
  3. Create a new Puppet certificate using Puppet certificate generate with the --dns-alt-names option:
    root@puppet:~# puppet certificate generate puppet 
    --dns-alt-names puppet.example.com,puppet.example.org,puppet.example.net --ca-location local Notice: puppet has a waiting certificate request true
  4. Sign the new certificate:
    root@puppet:~# puppet cert --allow-dns-alt-names sign puppet
    Notice: Signed certificate request for puppet
    Notice: Removing file Puppet::SSL::CertificateRequest puppet at '/var/lib/puppet/ssl/ca/requests/puppet.pem'
  5. Restart the Puppet master process:
    root@puppet:~# service puppetmaster restart
    [ ok ] Restarting puppet master.

How it works...

When your puppet agents connect to the Puppet server, they look for a host called Puppet, they then look for a host called Puppet.[your domain]. If your clients are in different domains, then you need your Puppet master to reply to all the names correctly. By removing the existing certificate and generating a new one, you can have your Puppet master reply to multiple DNS names.

Summary

Configuration management has become a requirement for system administrators. Knowing how to use configuration management tools, such as Puppet, enables administrators to take full advantage of automated provisioning systems and cloud resources. There is a natural progression from performing a task, scripting a task to creating a module in Puppet, or Puppetizing a task.

Resources for Article:


Further resources on this subject:


Modal Close icon
Modal Close icon