Docs: Learning Puppet — Variables, Conditionals, and Facts


Learning Puppet — Variables, Conditionals, and Facts

Begin

    $my_variable = "A bunch of text"
    notify {$my_variable:}

Yup, that’s a variable, all right.

Variables

Variables! You’ve almost definitely used variables before in some other programming or scripting language, so we’ll cover the basics very quickly. A more complete explanation of the syntax and behavior of variables is available in the variables chapter of the Puppet reference manual.

  • $variables always start with a dollar sign. You assign to variables with the = operator.
  • Variables can hold strings, numbers, booleans, arrays, hashes, and the special undef value. See the data types chapter of the Puppet reference manual for more information.
  • If you’ve never assigned a variable, you can actually still use it — its value will be undef.
  • You can use variables as the value for any resource attribute, or as the title of a resource.
  • You can also interpolate variables inside double-quoted strings. To distinguish a variable from the surrounding text, you can wrap its name in curly braces. ("This is the ${variable} name.") This isn’t mandatory, but it is recommended.
  • Every variable has two names:
    • A short local name
    • A long fully-qualified name

    Fully qualified variables look like $scope::variable. Top scope variables are the same, but their scope is nameless. (For example: $::top_scope_variable.)

  • If you reference a variable with its short name and it isn’t present in the local scope, Puppet will also check the global top scope; this means you can almost always refer to global variables with just their short names. You can see more about this in the scope chapter of the Puppet reference manual: scope in Puppet Enterprise 2.x and Puppet 2.7, scope in Puppet 3
  • You can only assign the same variable once in a given scope. In this way, they’re more like constants from other programming languages.
    $longthing = "Imagine I have something really long in here. Like an SSH key, let's say."

    file {'authorized_keys':
      path    => '/root/.ssh/authorized_keys',
      content => $longthing,
    }

Pretty easy.

Aside: Why Do Everyone’s Manifests Seem to Use $::ipaddress?

People who write manifests to share with the public often adopt the habit of always using the $::variable notation when referring to facts.

As mentioned above, the double-colon prefix specifies that a given variable should be found at top scope. This isn’t actually necessary, since variable lookup will always reach top scope anyway. (See the scope chapter of the Puppet reference manual.)

However, explicitly asking for top scope helps work around two issues that can make public code behave unpredictably. One issue affects all versions of Puppet 2.x, and the other affected earlier versions of Puppet 2.7.x:

  • In Puppet 2.x: if a user declares a class from a public module inside one of their own classes, and their personal class sets a variable whose name matches the name of a fact that the public class is trying to access, the public class will get the local variable instead of the fact. This will generally cause the public class to fail or do something really strange.
  • In earlier versions of Puppet 2.7.x: the dynamic scope deprecation warnings would sometimes be improperly triggered when manifests accessed top scopes variables without the double-colon prefix. This was fixed in later versions, but was very annoying for a while.

Neither of these issues are relevant as of Puppet 3, but not everyone is using Puppet 3 yet, and a Puppet 3-based version of Puppet Enterprise is still forthcoming later this year. Since a lot of people are still writing public code meant to be used with Puppet 2.7, you still see this idiom a lot.

Facts

Puppet has a bunch of built-in, pre-assigned variables that you can use. Check it out:

    # /root/examples/motd.pp

    file {'motd':
      ensure  => file,
      path    => '/etc/motd',
      mode    => 0644,
      content => "This Learning Puppet VM's IP address is ${ipaddress}. It thinks its
    hostname is ${fqdn}, but you might not be able to reach it there
    from your host machine. It is running ${operatingsystem} ${operatingsystemrelease} and
    Puppet ${puppetversion}.
    Web console login:
      URL: https://${ipaddress_eth0}
      User: puppet@example.com
      Password: learningpuppet
    ",
    }
# puppet apply /root/examples/motd.pp

notice: /Stage[main]//Host[puppet]/ensure: created
notice: /Stage[main]//File[motd]/ensure: defined content as '{md5}bb1a70a2a2ac5ed3cb83e1a8caa0e331'

# cat /etc/motd
This Learning Puppet VM's IP address is 172.16.52.135. It thinks its
hostname is learn.localdomain, but you might not be able to reach it there
from your host machine. It is running CentOS 5.7 and
Puppet 2.7.21 (Puppet Enterprise 2.8.1).
Web console login:
  URL: https://172.16.52.135
  User: puppet@example.com
  Password: learningpuppet

Our manifests are becoming more flexible, with pretty much no real work on our part.

What Are These Hostname and IPaddress Variables?

And where did they come from?

They’re “facts.” Puppet uses a tool called Facter, which discovers some system information, normalizes it into a set of variables, and passes them off to Puppet. Puppet’s compiler then has access to those facts when it’s reading a manifest.

Other Built-In Variables

In addition to the facts from Facter, Puppet has a few extra built-in variables. You can see a list of them in the variables chapter of the Puppet reference manual.

Conditional Statements

Puppet has several kinds of conditional statements. You can see more complete info about them in the conditional statements chapter of the Puppet reference manual..

By using facts as conditions, you can easily make Puppet do different things on different kinds of systems.

If

We’ll start with your basic if statement. Same as it ever was:

if condition {
  block of code
}
elsif condition {
  block of code
}
else {
  block of code
}
  • The else and any number of elsif statements are optional.
  • The blocks of code for each condition can contain any Puppet code.
  • The conditions can be any fragment of Puppet code that resolves to a boolean true/false value, including expressions, functions that return values, and variables. Follow the links for more detailed descriptions of expressions and functions.

An example if statement:

    if str2bool("$is_virtual") {
      service {'ntpd':
        ensure => stopped,
        enable => false,
      }
    }
    else {
      service { 'ntpd':
        name       => 'ntpd',
        ensure     => running,
        enable     => true,
        hasrestart => true,
        require => Package['ntp'],
      }
    }

Aside: Beware of the Fake False!

In the example above, we see something new: str2bool("$is_virtual").

The condition for an if statement has to resolve to a boolean true/false value. However, all facts are strings, and all non-empty strings — including the string "false" — are true. This means that facts that are “false” need to be transformed before Puppet will treat them as false.

In this case, we’re:

  • Surrounding the variable with double quotes — if it contained an actual boolean for some reason (and it usually wouldn’t), this would convert it to a string.
  • Passing the string to the str2bool function, which converts a string that looks like a boolean into a real true or false value.

The str2bool function is part of the puppetlabs/stdlib module, which is included with Puppet Enterprise. If you are running open source Puppet, you can install it by running sudo puppet module install puppetlabs/stdlib.

We could also use an expression instead: the expression $is_virtual == 'true' would resolve to true if the is_virtual fact has a value of true, and false otherwise.

Case

Another kind of conditional is the case statement. (Or switch, or whatever your language of choice calls it.)

    case $operatingsystem {
      centos: { $apache = "httpd" }
      # Note that these matches are case-insensitive.
      redhat: { $apache = "httpd" }
      debian: { $apache = "apache2" }
      ubuntu: { $apache = "apache2" }
      default: { fail("Unrecognized operating system for webserver") }
    }
    package {'apache':
      name   => $apache,
      ensure => latest,
    }

Instead of testing a condition up front, case matches a variable against a bunch of possible values. default is a special value, which does exactly what it sounds like.

In this example, we also see the fail function. Unlike the str2bool function above, fail doesn’t resolve to a value; instead, it fails compilation immediately with an error message.

Case matching

Case matches can be simple strings (like above), regular expressions, or comma-separated lists of either.

Here’s the example from above, rewritten to use comma-separated lists of strings:

    case $operatingsystem {
      centos, redhat: { $apache = "httpd" }
      debian, ubuntu: { $apache = "apache2" }
      default: { fail("Unrecognized operating system for webserver") }
    }

And here’s a regex example:

    case $ipaddress_eth0 {
      /^127[\d.]+$/: {
        notify {'misconfig':
          message => "Possible network misconfiguration: IP address of $0",
        }
      }
    }

String matching is case-insensitive, like the == comparison operator. Regular expressions are denoted with the slash-quoting used by Perl and Ruby; they’re case-sensitive by default, but you can use the (?i) and (?-i) switches to turn case-insensitivity on and off inside the pattern. Regex matches also assign captured subpatterns to $1, $2, etc. inside the associated code block, with $0 containing the whole matching string. See the regular expressions section of the Puppet reference manual’s data types page for more details.

Selectors

Selectors might be less familiar; they’re kind of like the common ternary operator, and kind of like the case statement.

Instead of choosing between a set of code blocks, selectors choose between a group of possible values. You can’t use them on their own; instead, they’re usually used to assign a variable.

    $apache = $operatingsystem ? {
      centos                => 'httpd',
      redhat                => 'httpd',
      /(?i)(ubuntu|debian)/ => 'apache2',
      default               => undef,
    }

Careful of the syntax, there: it looks kind of like we’re saying $apache = $operatingsystem, but we’re not. The question mark flags $operatingsystem as the control variable of a selector, and the actual value that gets assigned is determined by which option $operatingsystem matches. Also note how the syntax differs from the case syntax: it uses hash rockets and line-end commas instead of colons and blocks, and you can’t use lists of values in a match. (If you want to match against a list, you have to fake it with a regular expression.)

It can look a little awkward, but there are plenty of situations where it’s the most concise way to get a value sorted out; if you’re ever not comfortable with it, you can just use a case statement to assign the variable instead.

Selectors can also be used directly as values for a resource attribute, but try not to do that, because it gets ugly fast.

Exercises

Exercise: Build Environment

Use the $operatingsystem fact to write a manifest that installs a C build environment on Debian-based (“debian,” “ubuntu”) and Enterprise Linux-based (“centos,” “redhat”) machines. (Both types of system require the gcc package, but Debian-type systems also require build-essential.)

Exercise: Simple NTP

Write a manifest that installs and configures NTP for Debian-based and Enterprise Linux-based Linux systems. This will be a package/file/service pattern where both kinds of systems use the same package name (ntp), but you’ll be shipping different config files (Debian version, Red Hat version – remember the file type’s “source” attribute) and using different service names (ntp and ntpd, respectively).

Next

Next Lesson:

Now that your manifests can adapt to different kinds of systems, it’s time to start grouping resources and conditionals into meaningful units. Onward to classes, defined resource types, and modules!

Off-Road:

Since facts from every node show up in the console, Puppet Enterprise can be a powerful inventory tool. Download Puppet Enterprise for free, follow the quick start guide to get a small environment installed, then try browsing the console’s inventory for a central view of your operating system versions, hardware profiles, and more.

↑ Back to top