Docs: Learning — Variables, Conditionals, and Facts


Learning — Variables, Conditionals, and Facts

You can write manifests and order resources; now, add logic and flexibility with conditional statements and variables.


OrderingIndexModules (Part One)


Variables

Variables! I’m going to bet you pretty much know this drill, so let’s move a little faster:

  • $variables always start with a dollar sign. You assign to variables with the = operator.
  • Variables can hold strings, numbers, special values (false, undef…), arrays, and hashes.
  • If you’ve never assigned a variable, you can actually still use it — its value will be undef. (You can also explicitly assign undef as a value, although the use case for that is somewhat advanced.)
  • You can use variables as the value for any resource attribute, or as the title of a resource.
  • You can also interpolate variables inside strings, if you use double-quotes. To distinguish a ${variable} from the surrounding text, you must wrap its name in curly braces.
  • Every variable has a short local name and 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 top scope;1 this means you can almost always refer to global variables with just their short names.

    Note: Current versions of Puppet can log spurious warnings if you refer to top-scope variables without the $:: prefix. These are due to a bug, and will be fixed in a future version.

  • You can only assign the same variable once in a given scope.2
    $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.

Facts

And now, a teaspoon of magic.

Before you even start writing your manifests, Puppet builds you a stash of pre-assigned variables. Check it out:

    # hosts-simple.pp
    
    # Host type reference:
    # http://docs.puppetlabs.com/references/stable/type.html#host
    
    host {'self':
      ensure       => present,
      name         => $fqdn,
      host_aliases => ['puppet', $hostname],
      ip           => $ipaddress,
    }
    
    file {'motd':
      ensure  => file,
      path    => '/etc/motd',
      mode    => 0644,
      content => "Welcome to ${hostname},\na ${operatingsystem} island in the sea of ${domain}.\n",
    }
# puppet apply hosts-simple.pp

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

# cat /etc/motd
Welcome to learn,
a CentOS island in the sea of localdomain.

Our manifests are starting to get versatile, with pretty much no real work on our part.

Hostname? IPaddress?

So where did those helpful variables come from? They’re “facts.” Puppet ships with a tool called Facter, which ferrets out your system information, normalizes it into a set of variables, and passes them off to Puppet. The compiler then has access to those facts when it’s reading a manifest.

There are a lot of different facts, and the easiest way to get a list of them is to simply run facter at your VM’s command line. You’ll get back a long list of key/value pairs separated by the familiar => hash rocket. To use one of these facts in your manifests, just prepend a dollar sign to its name (along with a ::, because being explicit about namespaces is a good habit).

Most kinds of system will have at least a few facts that aren’t available on other kinds of system (e.g., try comparing Facter’s output on your CentOS VM to what it does on an OS X machine), and it can get fuzzier if you’re extending Facter with custom facts, but there’s a general core of facts that give you the same info everywhere. You’ll get a feel for them pretty quickly, and can probably guess most of them just by reading the list of names.

Conditional Statements

Puppet has a fairly complete complement of conditional syntaxes, and the info available in facts makes it really easy to code different behavior for different 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.

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

The blocks of code for each condition can contain any Puppet code.

What is False?

The Puppet language’s data types are kind of loose, and a lot of things tend to get represented internally as strings, so it’s worth calling out what exactly will be treated as false by an if statement:

  • undef (the value of an unassigned variable)
  • '' (the empty string)
  • false
  • Any expression that evaluates to false.

In particular, be aware that the numeral 0 and the string “false” are both true. This means that while you can use some variables alone as an if-condition, you can’t use facts that way. Facts are always read into Puppet as strings, so you need to test conceptually-boolean facts for their string value instead of their literal truth or falsehood.

Conditions

Conditions can get pretty sophisticated: you can use any valid expression in an if statement. Usually, this is going to mean using one of the standard comparison operators (==, !=, <, >, <=, >=), the regex match operators (=~ and !~), or the in operator (which tests whether the right operand contains the left one).

Case

Also probably familiar: 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") }
      # "fail" is a function. We'll get to those later.
    }
    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.

Case matching

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

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.

Here’s a regex example:

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

And here’s the example from above, rewritten and more readable:

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

Selectors

Selectors might be less familiar; they’re kind of like the 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-$1",
        # (Don't actually use that package name.)
      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 pivot 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: Use the $operatingsystem fact to write a manifest that installs a 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: 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).

(Use a second manifest to disable the NTP service after you’ve gotten this example working; NTP can behave kind of uselessly in a virtual machine.)

Next

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!

  1. It’s actually a little more complicated than that, but don’t worry about it for now. You can read up on it later.

  2. This has to do with the declarative nature of the Puppet language: the idea is that the order in which you read the file shouldn’t matter, so changing a value halfway through is illegal, since it would make the results order-dependent.

    In practice, this isn’t the full story, because you can’t currently read a variable from anywhere north of its assignment. We’re working on that.

↑ Back to top

Contents

  1. Variables
  2. Facts
    1. Hostname? IPaddress?
  3. Conditional Statements
    1. If
    2. Case
    3. Selectors
  4. Exercises
  5. Next

Download the Docs

Puppet docs download

Download Puppet Enterprise

Puppet Enterprise promo