{% include '/version.md' %}
- Learn about the role of conditional statements in writing flexible Puppet code.
- Learn the syntax of the
if
statement. - Use an
if
statement to intelligently manage a package dependency.
In this quest, we'll discuss conditional statements. Conditional statements let you write Puppet code that will behave differently in different contexts.
To start this quest enter the following command:
quest begin conditional_statements
Because Puppet manages configurations on a variety of systems fulfilling a variety of roles, great Puppet code means flexible and portable Puppet code. Though Puppet's types and providers can translate between Puppet's resource syntax and the native tools on a wide variety of systems, there are still a lot of questions that you as a Puppet module developer will need to answer for yourself.
A good rule of thumb is that the resource abstraction layer answers how questions, while the Puppet code itself answers what questions.
As an example, let's take a look at the puppetlabs-apache
module. While this
module's developers rely on Puppet's providers to determine how the Apache
package is installed—whether it's handled by yum
, apt-get
or another
package manager—Puppet doesn't have any way of automatically determining the
name of the package that needs to be installed. Depending on whether the module
is used to manage Apache on a RedHat or Debian system, it will need to manage
either the httpd
or apache2
package.
This kind of what question is often addressed through a combination of
conditional statements and facts or parameters. If you look at the
puppetlabs-apache
module on the Forge, you'll see this
package name and numerous other
variables are set based on various conditional statements.
Simplified to show only the values we're concerned with, the conditional statement looks like this:
if $::osfamily == 'RedHat' {
...
$apache_name = 'httpd'
...
} elsif $::osfamily == 'Debian' {
...
$apache_name = 'apache2'
...
}
Here, the $apache_name
variable is set to either httpd
or apache2
depending on the value of the $::osfamily
fact. Elsewhere in the module,
you'll find a package
resource
that uses this $apache_name
variable to set its name
parameter.
package { 'httpd':
ensure => $ensure,
name => $::apache::apache_name,
notify => Class['Apache::Service'],
}
Remember that because the name
parameter is being explicitly set here, the
resource title (httpd
) only serves as an internal identifier for the
resource—it doesn't actually determine the name of the package that will be
installed.
Now that you've seen this real-world example of how and why a conditional statement can be used to create more flexible Puppet code, let's take a moment to discuss how these statements work and how to write them.
Conditional statements return different values or execute different blocks of code depending on the value of a specified variable.
Puppet supports a few different ways of implementing conditional logic:
if
statements,unless
statements,- case statements, and
- selectors.
Because the same concept underlies these different forms of conditional logic
available in Puppet, we'll only cover the if
statement in the tasks for this
quest. Once you have a good understanding of how to implement if
statements,
we'll leave you with descriptions of the other forms and some notes on when you
may find them useful.
An if
statement includes a condition followed by a block of Puppet code that
will only be executed if that condition evaluates as true. Optionally,
an if
statement can also include any number of elsif
clauses and an else
clause.
- If the
if
condition fails, Puppet moves on to theelsif
condition (if one exists). - If both the
if
and allelsif
conditions fail, Puppet will execute the code in theelse
clause (if one exists). - If all the conditions fail, and there is no
else
block, Puppet will do nothing and move on.
Let's return to our Pasture example module. The application is built on the Sinatra framework. Out of the box, Sinatra supports a few different options for the server the service will run: WEBrick, Thin, or Mongrel. In production, you would likely use Sinatra with a more robust option such as Passenger or Unicorn, but these built-in options will be adequate for this lesson.
We can easily select which server will be used in the Pasture application's
configuration file. However, options other than the default WEBrick are not
included as pre-requisites when we install the Pasture package. To use these
other options, we'll need our module to manage them as separate package
resources. However, we don't want to install these extra packages if we don't
plan on using them. Using the if
statement discussed above, we'll configure
the pasture
class to manage the necessary Thin or Mongrel package resource
only if one of these servers is selected.
By using a class parameter to specify the preferred server for Sinatra to use, we can use the same value to pass on to our configuration file template and to decide which additional packages need to be installed. Using parameters in this way helps keep configuration coordinated across all the components of the system your module is written to manage.
Task 1:
Open the module's init.pp
manifest.
vim pasture/manifests/init.pp
First, add a $sinatra_server
parameter with a default value of webrick
. The
beginning of your class should look like the following example:
class pasture (
$port = '80',
$default_character = 'sheep',
$default_message = '',
$pasture_config_file = '/etc/pasture_config.yaml',
$sinatra_server = 'webrick',
){
Next add the $sinatra_server
variable to the $pasture_config_hash
so that
it can be passed through to the configuration file template.
$pasture_config_hash = {
'port' => $port,
'default_character' => $default_character,
'default_message' => $default_message,
'sinatra_server' => $sinatra_server,
}
Task 2:
Once that's complete, open the pasture_config.yaml.epp
template.
vim pasture/templates/pasture_config.yaml.epp
Add the $sinatra_server
variable to the params block at the beginning of the
template. The Pasture application passes any settings under the
:sinatra_settings:
key to Sinatra itself.
<%- | $port,
$default_character,
$default_message,
$sinatra_server,
| -%>
# This file is managed by Puppet. Please do not make manual changes.
---
:default_character: <%= $default_character %>
:default_message: <%= $default_message %>
:sinatra_settings:
:port: <%= $port %>
:server: <%= $sinatra_server %>
Now that your module is able to manage this setting, add a conditional statement to manage the required packages for the Thin and Mongrel webservers.
Task 3:
Return to your init.pp
manifest.
vim pasture/manifests/init.pp
You can wrap a package resource in an if
statement to tell your class to only
manage the resource if the $sinatra_server
variable is thin
or mongrel
.
Both of these servers are available as gems, so you will use the gem
provider for the package.
Finally, we will add a notify
parameter pointing to our service resource.
This will ensure that the server package is managed before the service, and
that any updates to the package will trigger a restart of the service. Your
class should look like the example below, with the conditional statement to
manage your server packages included at the end.
class pasture (
$port = '80',
$default_character = 'sheep',
$default_message = '',
$pasture_config_file = '/etc/pasture_config.yaml',
$sinatra_server = 'webrick',
){
package { 'pasture':
ensure => present,
provider => 'gem',
before => File[$pasture_config_file],
}
$pasture_config_hash = {
'port' => $port,
'default_character' => $default_character,
'default_message' => $default_message,
'sinatra_server' => $sinatra_server,
}
file { $pasture_config_file:
content => epp('pasture/pasture_config.yaml.epp', $pasture_config_hash),
notify => Service['pasture'],
}
$pasture_service_hash = {
'pasture_config_file' => $pasture_config_file,
}
file { '/etc/systemd/system/pasture.service':
content => epp('pasture/pasture.service.epp', $pasture_service_hash),
notify => Service['pasture'],
}
service { 'pasture':
ensure => running,
}
if ($sinatra_server == 'thin') or ($sinatra_server == 'mongrel') {
package { $sinatra_server:
provider => 'gem',
notify => Service['pasture'],
}
}
}
With these changes to your class, you can easily accommodate different servers for different agent nodes in your infrastructure. For example, you may want to use the default WEBrick server on a development system and the Thin server on for production.
We created two new systems for this quest: pasture-dev.puppet.vm
and
pasture-prod.puppet.vm
. For a more complex infrastructure, you would likely
manage your development, test, and production segments of your infrastructure
by creating a distinct environment
for each. For now, however, we can easily demonstrate our conditional statement
by setting up two different node definitions in the site.pp
manifest.
Task 4:
vim /etc/puppetlabs/code/environments/production/manifests/site.pp
node 'pasture-dev.puppet.vm' {
include pasture
}
node 'pasture-prod.puppet.vm' {
class { 'pasture':
sinatra_server => 'thin',
}
}
Now that you're working across multiple nodes, connecting manually with SSH to
trigger Puppet runs may start to seem a little tedious. The puppet job
tool
lets you trigger Puppet runs across multiple nodes remotely.
Task 5:
Before using this tool, we'll have to take a few steps via the PE console to set up authentication through PE's role-based access control system (RBAC).
To log in to the console, bring up a web browser on the host system you're
using to run the Learning VM and navigate to https://<VM's IPADDRESS>
. (Note
that using https
will take you to the console, while http
will take you to
this quest guide.)
You may see a warning from your browser that the PE console is using a self-signed certificate. You can safely ignore this warning and proceed to the PE console login page. (You may have to click on an advanced option for the option to proceed.)
Use the following credentials to log in:
Username: admin
Password: puppetlabs
Once you're connected, click the Access control menu option in the navigation bar, then click the Users tab.
Enter a new user with the Full name Learning
and Login learning
and click Add local user
.
Click on the name of the new user, then click the Generate password reset link. Copy the given link to a new browser tab and set the password to: puppet.
Return to the original PE console browser tab, and click the Access Control menu item, then click the User Roles tab. Click on the link for the Operators role. Select the Learning user from the dropdown menu, click the Add user button, and finally click the Commit 1 change button near the bottom right of the console screen. Close the new browser tab that was used to reset the user password.
With this user configured, you can use the puppet access
command to generate
a token that will allow you to use the puppet job
tool. We'll set the
lifetime for this token to one day so you won't have to worry about
re-authenticating as you work.
puppet access login --lifetime 1d
When prompted, supply the username learning and password puppet.
Now you can trigger Puppet agent runs on pasture-dev.puppet.vm
and
pasture-prod.puppet.vm
with the puppet job
tool. We provide the names of
the two nodes in a comma-separated list after the --nodes
flag. (Note that
there is no space between the node names, which can make it a little hard to
tell the difference between a comma that separates node names and the dots in
the node names themselves.)
puppet job run --nodes pasture-dev.puppet.vm,pasture-prod.puppet.vm
When the jobs complete, take a moment to check each with a curl
command.
curl 'pasture-dev.puppet.vm/api/v1/cowsay?message=Hello!'
and
curl 'pasture-prod.puppet.vm/api/v1/cowsay?message=Hello!'
To verify that each system is running the server you specified, you can log
in and use the journalctl
command to check the service's startup log.
And run
journalctl -u pasture
Disconnect from pasture-dev
exit
And connect to the next system:
Check the log here as well:
journalctl -u pasture
Now that you've verified that each node is running the expected server, disconnect to return to the Learning VM.
exit
In this quest, you learned how conditional statements can help make the code
you write for a Puppet module adaptable to cover different conditions. You saw
an example of how the puppetlabs-apache
module uses a conditional statement
to install a different package depending on the node's operating system. After
learning about the syntax of the if
statement, you incorporated a conditional
statement into your pasture
module to help manage the package dependencies
for the different server options available to the Sinatra framework.
So far, the pasture
and motd
modules you've used have been written from
scratch. This is great for the sake of learning, but one of Puppet's strengths
lies in the Puppet community and the Puppet Forge—the repository of pre-written
modules you can easily incorporate into your own Puppet codebase. In the next
quest, you'll see how to make use of an existing module to set up a database
backend for the Pasture application.
- Puppet's docs page covers the syntax of all forms of conditional statements and expressions.
- The style guide includes best-practices recommendations for the use of conditionals.