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 3. Writing Better Manifests

 

"Measuring programming progress by lines of code is like measuring aircraft building progress by weight."

 
 --Bill Gates

In this chapter, we will cover:

  • Using arrays of resources
  • Using resource defaults
  • Using defined types
  • Using tags
  • Using run stages
  • Using roles and profiles
  • Passing parameters to classes
  • Passing parameters from Hiera
  • Writing reusable, cross-platform manifests
  • Getting information about the environment
  • Importing dynamic information
  • Passing arguments to shell commands

Introduction

Your Puppet manifests are the living documentation for your entire infrastructure. Keeping them tidy and well organized is a great way to make it easier to maintain and understand. Puppet gives you a number of tools to do this, as follows:

  • Arrays
  • Defaults
  • Defined types
  • Dependencies
  • Class parameters

We'll see how to use all of these and more. As you read through the chapter, try out the examples and look through your own manifests to see where these features might help you simplify and improve your Puppet code.

Using arrays of resources

Anything that you can do to a resource, you can do to an array of resources. Use this idea to refactor your manifests to make them shorter and clearer.

How to do it…

Here are the steps to refactor using arrays of resources:

  1. Identify a class in your manifest where you have several instances of the same kind of resource, for example, packages:
      package { 'sudo' : ensure => installed }
      package { 'unzip' : ensure => installed }
      package { 'locate' : ensure => installed }
      package { 'lsof' : ensure => installed }
      package { 'cron' : ensure => installed }
      package { 'rubygems' : ensure => installed }
  2. Group them together and replace them with a single package resource using an array:
      package
      {
        [ 'cron',
        'locate',
        'lsof',
        'rubygems',
        'sudo',
        'unzip' ]:
        ensure => installed,
      }

How it works…

Most of Puppet's resource types can accept an array instead of a single name, and will create one instance for each of the elements in the array. All the parameters you provide for the resource (for example, ensure => installed) will be assigned to each of the new resource instances. This shorthand will only work when all the resources have the same attributes.

See also

  • The Iterating over multiple items recipe in Chapter 1, Puppet Language and Style
How to do it…

Here are the steps to refactor using arrays of resources:

Identify a class in your manifest where you have several instances of the same kind of resource, for example, packages:
  package { 'sudo' : ensure => installed }
  package { 'unzip' : ensure => installed }
  package { 'locate' : ensure => installed }
  package { 'lsof' : ensure => installed }
  package { 'cron' : ensure => installed }
  package { 'rubygems' : ensure => installed }
Group them together and replace them with a single package resource using an array:
  package
  {
    [ 'cron',
    'locate',
    'lsof',
    'rubygems',
    'sudo',
    'unzip' ]:
    ensure => installed,
  }

How it works…

Most of Puppet's resource types can accept an array instead of a single name, and will create one instance for each of the elements in the array. All the parameters you provide for the resource (for example, ensure => installed) will be assigned to each of the new resource instances. This shorthand will only work when all the resources have the same attributes.

See also

  • The Iterating over multiple items recipe in Chapter 1, Puppet Language and Style
How it works…

Most of Puppet's

resource types can accept an array instead of a single name, and will create one instance for each of the elements in the array. All the parameters you provide for the resource (for example, ensure => installed) will be assigned to each of the new resource instances. This shorthand will only work when all the resources have the same attributes.

See also

  • The Iterating over multiple items recipe in Chapter 1, Puppet Language and Style
See also

The Iterating over multiple items recipe in
  • Chapter 1, Puppet Language and Style

Using resource defaults

A Puppet module is a group of related resources, usually grouped to configure a specific service. Within a module, you may define multiple resources; resource defaults allow you to specify the default attribute values for a resource. In this example, we'll show you how to specify a resource default for the File type.

How to do it...

To show you how to use resource defaults, we'll create an apache module. Within this module we will specify that the default owner and group are the apache user as follows:

  1. Create an apache module and create a resource default for the File type:
      class apache {
        File {
          owner => 'apache',
          group => 'apache',
          mode => 0644,
        }
      }
  2. Create html files within the /var/www/html directory:
      file {'/var/www/html/index.html':
        content => "<html><body><h1><a
          href='cookbook.html'>Cookbook!
          </a></h1></body></html>\n",
      }
      file {'/var/www/html/cookbook.html':
        content =>
          "<html><body><h2>PacktPub</h2></body></html>\n",
      }
    
  3. Add this class to your default node definition, or use puppet apply to apply the module to your node. I will use the method we configured in the previous chapter, pushing our code to the Git repository and using a Git hook to have the code deployed to the Puppet master as follows:
    t@mylaptop ~/puppet $ git pull origin production
    From git.example.com:repos/puppet
     * branch            production -> FETCH_HEAD
    Already up-to-date.
    t@mylaptop ~/puppet $ cd modules
    t@mylaptop ~/puppet/modules $ mkdir -p apache/manifests
    t@mylaptop ~/puppet/modules $ vim apache/manifests/init.pp
    t@mylaptop ~/puppet/modules $ cd ..
    t@mylaptop ~/puppet $ vim manifests/site.pp 
    t@mylaptop ~/puppet $ git status
    On branch production
    Changes not staged for commit:
    modified:   manifests/site.pp
    Untracked files:
    modules/apache/
    t@mylaptop ~/puppet $ git add manifests/site.pp modules/apache
    t@mylaptop ~/puppet $ git commit -m 'adding apache module'
    [production d639a86] adding apache module
     2 files changed, 14 insertions(+)
     create mode 100644 modules/apache/manifests/init.pp
    t@mylaptop ~/puppet $ git push origin production
    Counting objects: 13, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (6/6), done.
    Writing objects: 100% (8/8), 885 bytes | 0 bytes/s, done.
    Total 8 (delta 0), reused 0 (delta 0)
    remote: To puppet@puppet.example.com:/etc/puppet/environments/puppet.git
    remote:    832f6a9..d639a86  production -> production
    remote: Already on 'production'
    remote: From /etc/puppet/environments/puppet
    remote:    832f6a9..d639a86  production -> origin/production
    remote: Updating 832f6a9..d639a86
    remote: Fast-forward
    remote:  manifests/site.pp                |    1 +
    remote:  modules/apache/manifests/init.pp |   13 +++++++++++++
    remote:  2 files changed, 14 insertions(+)
    remote:  create mode 100644 modules/apache/manifests/init.pp
    To git@git.example.com:repos/puppet.git
       832f6a9..d639a86  production -> production
    
  4. Apply the module to a node or run Puppet:
    Notice: /Stage[main]/Apache/File[/var/www/html/cookbook.html]/ensure: defined content as '{md5}493473fb5bde778ca93d034900348c5d'
    Notice: /Stage[main]/Apache/File[/var/www/html/index.html]/ensure: defined content as '{md5}184f22c181c5632b86ebf9a0370685b3'
    Notice: Finished catalog run in 2.00 seconds
    [root@hiera-test ~]# ls -l /var/www/html
    total 8
    -rw-r--r--. 1 apache apache 44 Sep 15 12:00 cookbook.html
    -rw-r--r--. 1 apache apache 73 Sep 15 12:00 index.html
    

How it works...

The resource default we defined specifies the owner, group, and mode for all file resources within this class (also known as within this scope). Unless you specifically override a resource default, the value for an attribute will be taken from the default.

There's more...

You can specify resource defaults for any resource type. You can also specify resource defaults in site.pp. I find it useful to specify the default action for Package and Service resources as follows:

  Package { ensure => 'installed' }
  Service {
    hasrestart => true,
    enable     => true,
    ensure     => true,
  }

With these defaults, whenever you specify a package, the package will be installed. Whenever you specify a service, the service will be started and enabled to run at boot. These are the usual reasons you specify packages and services, most of the time these defaults will do what you prefer and your code will be cleaner. When you need to disable a service, simply override the defaults.

How to do it...

To show you how to use

resource defaults, we'll create an apache module. Within this module we will specify that the default owner and group are the apache user as follows:

  1. Create an apache module and create a resource default for the File type:
      class apache {
        File {
          owner => 'apache',
          group => 'apache',
          mode => 0644,
        }
      }
  2. Create html files within the /var/www/html directory:
      file {'/var/www/html/index.html':
        content => "<html><body><h1><a
          href='cookbook.html'>Cookbook!
          </a></h1></body></html>\n",
      }
      file {'/var/www/html/cookbook.html':
        content =>
          "<html><body><h2>PacktPub</h2></body></html>\n",
      }
    
  3. Add this class to your default node definition, or use puppet apply to apply the module to your node. I will use the method we configured in the previous chapter, pushing our code to the Git repository and using a Git hook to have the code deployed to the Puppet master as follows:
    t@mylaptop ~/puppet $ git pull origin production
    From git.example.com:repos/puppet
     * branch            production -> FETCH_HEAD
    Already up-to-date.
    t@mylaptop ~/puppet $ cd modules
    t@mylaptop ~/puppet/modules $ mkdir -p apache/manifests
    t@mylaptop ~/puppet/modules $ vim apache/manifests/init.pp
    t@mylaptop ~/puppet/modules $ cd ..
    t@mylaptop ~/puppet $ vim manifests/site.pp 
    t@mylaptop ~/puppet $ git status
    On branch production
    Changes not staged for commit:
    modified:   manifests/site.pp
    Untracked files:
    modules/apache/
    t@mylaptop ~/puppet $ git add manifests/site.pp modules/apache
    t@mylaptop ~/puppet $ git commit -m 'adding apache module'
    [production d639a86] adding apache module
     2 files changed, 14 insertions(+)
     create mode 100644 modules/apache/manifests/init.pp
    t@mylaptop ~/puppet $ git push origin production
    Counting objects: 13, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (6/6), done.
    Writing objects: 100% (8/8), 885 bytes | 0 bytes/s, done.
    Total 8 (delta 0), reused 0 (delta 0)
    remote: To puppet@puppet.example.com:/etc/puppet/environments/puppet.git
    remote:    832f6a9..d639a86  production -> production
    remote: Already on 'production'
    remote: From /etc/puppet/environments/puppet
    remote:    832f6a9..d639a86  production -> origin/production
    remote: Updating 832f6a9..d639a86
    remote: Fast-forward
    remote:  manifests/site.pp                |    1 +
    remote:  modules/apache/manifests/init.pp |   13 +++++++++++++
    remote:  2 files changed, 14 insertions(+)
    remote:  create mode 100644 modules/apache/manifests/init.pp
    To git@git.example.com:repos/puppet.git
       832f6a9..d639a86  production -> production
    
  4. Apply the module to a node or run Puppet:
    Notice: /Stage[main]/Apache/File[/var/www/html/cookbook.html]/ensure: defined content as '{md5}493473fb5bde778ca93d034900348c5d'
    Notice: /Stage[main]/Apache/File[/var/www/html/index.html]/ensure: defined content as '{md5}184f22c181c5632b86ebf9a0370685b3'
    Notice: Finished catalog run in 2.00 seconds
    [root@hiera-test ~]# ls -l /var/www/html
    total 8
    -rw-r--r--. 1 apache apache 44 Sep 15 12:00 cookbook.html
    -rw-r--r--. 1 apache apache 73 Sep 15 12:00 index.html
    

How it works...

The resource default we defined specifies the owner, group, and mode for all file resources within this class (also known as within this scope). Unless you specifically override a resource default, the value for an attribute will be taken from the default.

There's more...

You can specify resource defaults for any resource type. You can also specify resource defaults in site.pp. I find it useful to specify the default action for Package and Service resources as follows:

  Package { ensure => 'installed' }
  Service {
    hasrestart => true,
    enable     => true,
    ensure     => true,
  }

With these defaults, whenever you specify a package, the package will be installed. Whenever you specify a service, the service will be started and enabled to run at boot. These are the usual reasons you specify packages and services, most of the time these defaults will do what you prefer and your code will be cleaner. When you need to disable a service, simply override the defaults.

How it works...

The resource default we defined specifies the owner, group, and mode for all file resources within this class (also known as within this scope). Unless you specifically override a resource default, the value for an attribute will be taken from the default.

There's more...

You can specify resource defaults for any resource type. You can also specify resource defaults in site.pp. I find it useful to specify the default action for Package and Service resources as follows:

  Package { ensure => 'installed' }
  Service {
    hasrestart => true,
    enable     => true,
    ensure     => true,
  }

With these defaults, whenever you specify a package, the package will be installed. Whenever you specify a service, the service will be started and enabled to run at boot. These are the usual reasons you specify packages and services, most of the time these defaults will do what you prefer and your code will be cleaner. When you need to disable a service, simply override the defaults.

There's more...

You can specify resource defaults for any

resource type. You can also specify resource defaults in site.pp. I find it useful to specify the default action for Package and Service resources as follows:

  Package { ensure => 'installed' }
  Service {
    hasrestart => true,
    enable     => true,
    ensure     => true,
  }

With these defaults, whenever you specify a package, the package will be installed. Whenever you specify a service, the service will be started and enabled to run at boot. These are the usual reasons you specify packages and services, most of the time these defaults will do what you prefer and your code will be cleaner. When you need to disable a service, simply override the defaults.

Using defined types

In the previous example, we saw how to reduce redundant code by grouping identical resources into arrays. However, this technique is limited to resources where all the parameters are the same. When you have a set of resources that have some parameters in common, you need to use a defined type to group them together.

How to do it…

The following steps will show you how to create a definition:

  1. Add the following code to your manifest:
      define tmpfile() {
        file { "/tmp/${name}": content => "Hello, world\n",
        }
      }
      tmpfile { ['a', 'b', 'c']: }
  2. Run Puppet:
    [root@hiera-test ~]# vim tmp.pp
    [root@hiera-test ~]# puppet apply tmp.pp 
    Notice: Compiled catalog for hiera-test.example.com in environment production in 0.11 seconds
    Notice: /Stage[main]/Main/Tmpfile[a]/File[/tmp/a]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850'
    Notice: /Stage[main]/Main/Tmpfile[b]/File[/tmp/b]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850'
    Notice: /Stage[main]/Main/Tmpfile[c]/File[/tmp/c]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850'
    Notice: Finished catalog run in 0.09 seconds
    [root@hiera-test ~]# cat /tmp/{a,b,c}
    Hello, world
    Hello, world
    Hello, world
    

How it works…

You can think of a defined type (introduced with the define keyword) as a cookie-cutter. It describes a pattern that Puppet can use to create lots of similar resources. Any time you declare a tmpfile instance in your manifest, Puppet will insert all the resources contained in the tmpfile definition.

In our example, the definition of tmpfile contains a single file resource whose content is Hello, world\n and whose path is /tmp/${name}. If you declared an instance of tmpfile with the name foo:

tmpfile { 'foo': }

Puppet will create a file with the path /tmp/foo. In other words, ${name} in the definition will be replaced by the name of any actual instance that Puppet is asked to create. It's almost as though we created a new kind of resource: tmpfile, which has one parameter—its name.

Just like with regular resources, we don't have to pass just one title; as in the preceding example, we can provide an array of titles and Puppet will create as many resources as required.

Tip

A word on name, the namevar: Every resource you create must have a unique name, the namevar. This is different than the title, which is how puppet refers to the resource internally (although they are often the same).

There's more…

In the example, we created a definition where the only parameter that varies between instances is the name parameter. But we can add whatever parameters we want, so long as we declare them in the definition in parentheses after the name parameter, as follows:

  define tmpfile($greeting) {
    file { "/tmp/${name}": content => $greeting,
    }
  }

Next, pass values to them when we declare an instance of the resource:

  tmpfile{ 'foo':
    greeting => "Good Morning\n",
  }

You can declare multiple parameters as a comma-separated list:

  define webapp($domain,$path,$platform) {
    ...
  }
  webapp { 'mywizzoapp':
    domain   => 'mywizzoapp.com',
    path     => '/var/www/apps/mywizzoapp',
    platform => 'Rails',
  }

You can also declare default values for any parameters that aren't supplied, thus making them optional:

  define tmpfile($greeting,$mode='0644') {
    ...
  }

This is a powerful technique for abstracting out everything that's common to certain resources, and keeping it in one place so that you don't repeat yourself. In the preceding example, there might be many individual resources contained within webapp: packages, config files, source code checkouts, virtual hosts, and so on. But all of them are the same for every instance of webapp except the parameters we provide. These might be referenced in a template, for example, to set the domain for a virtual host.

See also

  • The Passing parameters to classes recipe, in this chapter
How to do it…

The following steps will show you how to create a definition:

Add the following code to your manifest:
  define tmpfile() {
    file { "/tmp/${name}": content => "Hello, world\n",
    }
  }
  tmpfile { ['a', 'b', 'c']: }
Run Puppet:
[root@hiera-test ~]# vim tmp.pp
[root@hiera-test ~]# puppet apply tmp.pp 
Notice: Compiled catalog for hiera-test.example.com in environment production in 0.11 seconds
Notice: /Stage[main]/Main/Tmpfile[a]/File[/tmp/a]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850'
Notice: /Stage[main]/Main/Tmpfile[b]/File[/tmp/b]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850'
Notice: /Stage[main]/Main/Tmpfile[c]/File[/tmp/c]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850'
Notice: Finished catalog run in 0.09 seconds
[root@hiera-test ~]# cat /tmp/{a,b,c}
Hello, world
Hello, world
Hello, world

How it works…

You can think of a defined type (introduced with the define keyword) as a cookie-cutter. It describes a pattern that Puppet can use to create lots of similar resources. Any time you declare a tmpfile instance in your manifest, Puppet will insert all the resources contained in the tmpfile definition.

In our example, the definition of tmpfile contains a single file resource whose content is Hello, world\n and whose path is /tmp/${name}. If you declared an instance of tmpfile with the name foo:

tmpfile { 'foo': }

Puppet will create a file with the path /tmp/foo. In other words, ${name} in the definition will be replaced by the name of any actual instance that Puppet is asked to create. It's almost as though we created a new kind of resource: tmpfile, which has one parameter—its name.

Just like with regular resources, we don't have to pass just one title; as in the preceding example, we can provide an array of titles and Puppet will create as many resources as required.

Tip

A word on name, the namevar: Every resource you create must have a unique name, the namevar. This is different than the title, which is how puppet refers to the resource internally (although they are often the same).

There's more…

In the example, we created a definition where the only parameter that varies between instances is the name parameter. But we can add whatever parameters we want, so long as we declare them in the definition in parentheses after the name parameter, as follows:

  define tmpfile($greeting) {
    file { "/tmp/${name}": content => $greeting,
    }
  }

Next, pass values to them when we declare an instance of the resource:

  tmpfile{ 'foo':
    greeting => "Good Morning\n",
  }

You can declare multiple parameters as a comma-separated list:

  define webapp($domain,$path,$platform) {
    ...
  }
  webapp { 'mywizzoapp':
    domain   => 'mywizzoapp.com',
    path     => '/var/www/apps/mywizzoapp',
    platform => 'Rails',
  }

You can also declare default values for any parameters that aren't supplied, thus making them optional:

  define tmpfile($greeting,$mode='0644') {
    ...
  }

This is a powerful technique for abstracting out everything that's common to certain resources, and keeping it in one place so that you don't repeat yourself. In the preceding example, there might be many individual resources contained within webapp: packages, config files, source code checkouts, virtual hosts, and so on. But all of them are the same for every instance of webapp except the parameters we provide. These might be referenced in a template, for example, to set the domain for a virtual host.

See also

  • The Passing parameters to classes recipe, in this chapter
How it works…

You can think of a defined

type (introduced with the define keyword) as a cookie-cutter. It describes a pattern that Puppet can use to create lots of similar resources. Any time you declare a tmpfile instance in your manifest, Puppet will insert all the resources contained in the tmpfile definition.

In our example, the definition of tmpfile contains a single file resource whose content is Hello, world\n and whose path is /tmp/${name}. If you declared an instance of tmpfile with the name foo:

tmpfile { 'foo': }

Puppet will create a file with the path /tmp/foo. In other words, ${name} in the definition will be replaced by the name of any actual instance that Puppet is asked to create. It's almost as though we created a new kind of resource: tmpfile, which has one parameter—its name.

Just like with regular resources, we don't have to pass just one title; as in the preceding example, we can provide an array of titles and Puppet will create as many resources as required.

Tip

A word on name, the namevar: Every resource you create must have a unique name, the namevar. This is different than the title, which is how puppet refers to the resource internally (although they are often the same).

There's more…

In the example, we created a definition where the only parameter that varies between instances is the name parameter. But we can add whatever parameters we want, so long as we declare them in the definition in parentheses after the name parameter, as follows:

  define tmpfile($greeting) {
    file { "/tmp/${name}": content => $greeting,
    }
  }

Next, pass values to them when we declare an instance of the resource:

  tmpfile{ 'foo':
    greeting => "Good Morning\n",
  }

You can declare multiple parameters as a comma-separated list:

  define webapp($domain,$path,$platform) {
    ...
  }
  webapp { 'mywizzoapp':
    domain   => 'mywizzoapp.com',
    path     => '/var/www/apps/mywizzoapp',
    platform => 'Rails',
  }

You can also declare default values for any parameters that aren't supplied, thus making them optional:

  define tmpfile($greeting,$mode='0644') {
    ...
  }

This is a powerful technique for abstracting out everything that's common to certain resources, and keeping it in one place so that you don't repeat yourself. In the preceding example, there might be many individual resources contained within webapp: packages, config files, source code checkouts, virtual hosts, and so on. But all of them are the same for every instance of webapp except the parameters we provide. These might be referenced in a template, for example, to set the domain for a virtual host.

See also

  • The Passing parameters to classes recipe, in this chapter
There's more…

In the example, we created a definition where the only parameter that varies between instances is the name parameter. But we can add whatever parameters we want, so long as we declare them in the definition in parentheses after the name parameter, as follows:

define tmpfile($greeting) { file { "/tmp/${name}": content => $greeting, } }

Next, pass values to them when we declare an instance of the resource:

tmpfile{ 'foo': greeting => "Good Morning\n", }

You can declare multiple parameters as a comma-separated list:

define webapp($domain,$path,$platform) { ... } webapp { 'mywizzoapp': domain => 'mywizzoapp.com', path => '/var/www/apps/mywizzoapp', platform => 'Rails', }

You can also declare default values for any parameters that aren't supplied, thus making them optional:

define tmpfile($greeting,$mode='0644') { ... }

This is a powerful technique

for abstracting out everything that's common to certain resources, and keeping it in one place so that you don't repeat yourself. In the preceding example, there might be many individual resources contained within webapp: packages, config files, source code checkouts, virtual hosts, and so on. But all of them are the same for every instance of webapp except the parameters we provide. These might be referenced in a template, for example, to set the domain for a virtual host.

See also

  • The Passing parameters to classes recipe, in this chapter
See also

The Passing parameters to classes recipe, in this chapter

Using tags

Sometimes one Puppet class needs to know about another or at least to know whether or not it's present. For example, a class that manages the firewall may need to know whether or not the node is a web server.

Puppet's tagged function will tell you whether a named class or resource is present in the catalog for this node. You can also apply arbitrary tags to a node or class and check for the presence of these tags. Tags are another metaparameter, similar to require and notify we introduced in Chapter 1, Puppet Language and Style. Metaparameters are used in the compilation of the Puppet catalog but are not an attribute of the resource to which they are attached.

How to do it...

To help you find out if you're running on a particular node or class of nodes all nodes are automatically tagged with the node name and the names of any classes they include. Here's an example that shows you how to use tagged to get this information:

  1. Add the following code to your site.pp file (replacing cookbook with your machine's hostname):
      node 'cookbook' {
        if tagged('cookbook') {
          notify { 'tagged cookbook': }
        }
      }
  2. Run Puppet:
    root@cookbook:~# puppet agent -vt
    Info: Caching catalog for cookbook
    Info: Applying configuration version '1410848350'
    Notice: tagged cookbook
    Notice: Finished catalog run in 1.00 seconds
    

    Nodes are also automatically tagged with the names of all the classes they include in addition to several other automatic tags. You can use tagged to find out what classes are included on the node.

    You're not just limited to checking the tags automatically applied by Puppet. You can also add your own. To set an arbitrary tag on a node, use the tag function, as in the following example:

  3. Modify your site.pp file as follows:
      node 'cookbook' {
        tag('tagging')
        class {'tag_test': }
      }
  4. Add a tag_test module with the following init.pp (or be lazy and add the following definition to your site.pp):
      class tag_test {
        if tagged('tagging') {
          notify { 'containing node/class was tagged.': }
        }
      }
  5. Run Puppet:
    root@cookbook:~# puppet agent -vt
    Info: Caching catalog for cookbook
    Info: Applying configuration version '1410851300'
    Notice: containing node/class was tagged.
    Notice: Finished catalog run in 0.22 seconds
    
  6. You can also use tags to determine which parts of the manifest to apply. If you use the --tags option on the Puppet command line, Puppet will apply only those classes or resources tagged with the specific tags you include. For example, we can define our cookbook class with two classes:
      node cookbook {
        class {'first_class': }
        class {'second_class': }
      }
      class first_class {
        notify { 'First Class': }
      }
      class second_class {
        notify {'Second Class': }
      }
  7. Now when we run puppet agent on the cookbook node, we see both notifies:
    root@cookbook:~# puppet agent -t
    Notice: Second Class
    Notice: First Class
    Notice: Finished catalog run in 0.22 seconds
    
  8. Now apply the first_class and add --tags function to the command line:
    root@cookbook:~# puppet agent -t --tags first_class
    Notice: First Class
    Notice: Finished catalog run in 0.07 seconds
    

There's more…

You can use tags to create a collection of resources, and then make the collection a dependency for some other resource. For example, say some service depends on a config file that is built from a number of file snippets, as in the following example:

  class firewall::service {
    service { 'firewall': ... 
    }
    File <| tag == 'firewall-snippet' |> ~> Service['firewall'] 
  }
  class myapp { 
    file { '/etc/firewall.d/myapp.conf': tag => 'firewall-snippet', ... 
    } 
  }

Here, we've specified that the firewall service should be notified if any file resource tagged firewall-snippet is updated. All we need to do to add a firewall config snippet for any particular application or service is to tag it firewall-snippet, and Puppet will do the rest.

Although we could add a notify => Service["firewall"] function to each snippet resource if our definition of the firewall service were ever to change, we would have to hunt down and update all the snippets accordingly. The tag lets us encapsulate the logic in one place, making future maintenance and refactoring much easier.

Note

What's <| tag == 'firewall-snippet' |> syntax? This is called a resource collector, and it's a way of specifying a group of resources by searching for some piece of data about them; in this case, the value of a tag. You can find out more about resource collectors and the <| |> operator (sometimes known as the spaceship operator) on the Puppet Labs website: http://docs.puppetlabs.com/puppet/3/reference/lang_collectors.html.

How to do it...

To help you find out if you're running on a particular node or class of nodes all nodes are automatically tagged with the node name and the names of any classes they include. Here's an example that shows you how to use tagged to get this information:

Add the following code to your site.pp file (replacing cookbook with your machine's hostname):
  node 'cookbook' {
    if tagged('cookbook') {
      notify { 'tagged cookbook': }
    }
  }
Run Puppet:
root@cookbook:~# puppet agent -vt
Info: Caching catalog for cookbook
Info: Applying configuration version '1410848350'
Notice: tagged cookbook
Notice: Finished catalog run in 1.00 seconds
Nodes are
  1. also automatically tagged with the names of all the classes they include in addition to several other automatic tags. You can use tagged to find out what classes are included on the node.

    You're not just limited to checking the tags automatically applied by Puppet. You can also add your own. To set an arbitrary tag on a node, use the tag function, as in the following example:

  2. Modify your site.pp file as follows:
      node 'cookbook' {
        tag('tagging')
        class {'tag_test': }
      }
  3. Add a tag_test module with the following init.pp (or be lazy and add the following definition to your site.pp):
      class tag_test {
        if tagged('tagging') {
          notify { 'containing node/class was tagged.': }
        }
      }
  4. Run Puppet:
    root@cookbook:~# puppet agent -vt
    Info: Caching catalog for cookbook
    Info: Applying configuration version '1410851300'
    Notice: containing node/class was tagged.
    Notice: Finished catalog run in 0.22 seconds
    
  5. You can also use tags to determine which parts of the manifest to apply. If you use the --tags option on the Puppet command line, Puppet will apply only those classes or resources tagged with the specific tags you include. For example, we can define our cookbook class with two classes:
      node cookbook {
        class {'first_class': }
        class {'second_class': }
      }
      class first_class {
        notify { 'First Class': }
      }
      class second_class {
        notify {'Second Class': }
      }
  6. Now when we run puppet agent on the cookbook node, we see both notifies:
    root@cookbook:~# puppet agent -t
    Notice: Second Class
    Notice: First Class
    Notice: Finished catalog run in 0.22 seconds
    
  7. Now apply the first_class and add --tags function to the command line:
    root@cookbook:~# puppet agent -t --tags first_class
    Notice: First Class
    Notice: Finished catalog run in 0.07 seconds
    

There's more…

You can use tags to create a collection of resources, and then make the collection a dependency for some other resource. For example, say some service depends on a config file that is built from a number of file snippets, as in the following example:

  class firewall::service {
    service { 'firewall': ... 
    }
    File <| tag == 'firewall-snippet' |> ~> Service['firewall'] 
  }
  class myapp { 
    file { '/etc/firewall.d/myapp.conf': tag => 'firewall-snippet', ... 
    } 
  }

Here, we've specified that the firewall service should be notified if any file resource tagged firewall-snippet is updated. All we need to do to add a firewall config snippet for any particular application or service is to tag it firewall-snippet, and Puppet will do the rest.

Although we could add a notify => Service["firewall"] function to each snippet resource if our definition of the firewall service were ever to change, we would have to hunt down and update all the snippets accordingly. The tag lets us encapsulate the logic in one place, making future maintenance and refactoring much easier.

Note

What's <| tag == 'firewall-snippet' |> syntax? This is called a resource collector, and it's a way of specifying a group of resources by searching for some piece of data about them; in this case, the value of a tag. You can find out more about resource collectors and the <| |> operator (sometimes known as the spaceship operator) on the Puppet Labs website: http://docs.puppetlabs.com/puppet/3/reference/lang_collectors.html.

There's more…

You can use tags to create a collection of resources, and then make the collection a dependency for some other resource. For example, say some service depends on a config file that is built from a number of file snippets, as in the following example:

class firewall::service { service { 'firewall': ... } File <| tag == 'firewall-snippet' |> ~> Service['firewall'] } class myapp { file { '/etc/firewall.d/myapp.conf': tag => 'firewall-snippet', ... } }

Here, we've specified that the firewall service should be notified if any file resource tagged firewall-snippet is updated. All we need to do to add a firewall config snippet for any particular application or service is to tag it firewall-snippet, and Puppet will do the rest.

Although we could

add a notify => Service["firewall"] function to each snippet resource if our definition of the firewall service were ever to change, we would have to hunt down and update all the snippets accordingly. The tag lets us encapsulate the logic in one place, making future maintenance and refactoring much easier.

Note

What's <| tag == 'firewall-snippet' |> syntax? This is called a resource collector, and it's a way of specifying a group of resources by searching for some piece of data about them; in this case, the value of a tag. You can find out more about resource collectors and the <| |> operator (sometimes known as the spaceship operator) on the Puppet Labs website: http://docs.puppetlabs.com/puppet/3/reference/lang_collectors.html.

Using run stages

A common requirement is to apply a certain group of resources before other groups (for example, installing a package repository or a custom Ruby version), or after others (for example, deploying an application once its dependencies are installed). Puppet's run stages feature allows you to do this.

By default, all resources in your manifest are applied in a single stage named main. If you need a resource to be applied before all others, you can assign it to a new run stage that is specified to come before main. Similarly, you could define a run stage that comes after main. In fact, you can define as many run stages as you need and tell Puppet which order they should be applied in.

In this example, we'll use stages to ensure one class is applied first and another last.

How to do it…

Here are the steps to create an example of using run stages:

  1. Create the file modules/admin/manifests/stages.pp with the following contents:
      class admin::stages {
        stage { 'first': before => Stage['main'] }
        stage { 'last': require => Stage['main'] }
        class me_first {
          notify { 'This will be done first': }
        }
        class me_last {
          notify { 'This will be done last': }
        }
        class { 'me_first':
          stage => 'first',
        }
        class { 'me_last':
          stage => 'last',
        }
      }
  2. Modify your site.pp file as follows:
      node 'cookbook' {
        class {'first_class': }
        class {'second_class': }
        include admin::stages
      }
  3. Run Puppet:
    root@cookbook:~# puppet agent -t
    Info: Applying configuration version '1411019225'
    Notice: This will be done first
    Notice: Second Class
    Notice: First Class
    Notice: This will be done last
    Notice: Finished catalog run in 0.43 seconds
    

How it works…

Let's examine this code in detail to see what's happening. First, we declare the run stages first and last, as follows:

  stage { 'first': before => Stage['main'] }
  stage { 'last': require => Stage['main'] }

For the first stage, we've specified that it should come before main. That is, every resource marked as being in the first stage will be applied before any resource in the main stage (the default stage).

The last stage requires the main stage, so no resource in the last stage can be applied until after every resource in the main stage.

We then declare some classes that we'll later assign to these run stages:

  class me_first {
    notify { 'This will be done first': }
  }
  class me_last {
    notify { 'This will be done last': }
  }

We can now put it all together and include these classes on the node, specifying the run stages for each as we do so:

  class { 'me_first': stage => 'first',
  }
  class { 'me_last': stage => 'last',
  }

Note that in the class declarations for me_first and me_last, we didn't have to specify that they take a stage parameter. The stage parameter is another metaparameter, which means it can be applied to any class or resource without having to be explicitly declared. When we ran puppet agent on our Puppet node, the notify from the me_first class was applied before the notifies from first_class and second_class. The notify from me_last was applied after the main stage, so it comes after the two notifies from first_class and second_class. If you run puppet agent multiple times, you will see that the notifies from first_class and second_class may not always appear in the same order but the me_first class will always come first and the me_last class will always come last.

There's more…

You can define as many run stages as you like, and set up any ordering for them. This can greatly simplify a complicated manifest that would otherwise require lots of explicit dependencies between resources. Beware of accidentally introducing dependency cycles, though; when you assign something to a run stage you're automatically making it dependent on everything in prior stages.

You may like to define your stages in the site.pp file instead, so that at the top level of the manifest, it's easy to see what stages are available.

Gary Larizza has written a helpful introduction to using run stages, with some real-world examples, on his website:

http://garylarizza.com/blog/2011/03/11/using-run-stages-with-puppet/

A caveat: many people don't like to use run stages, feeling that Puppet already provides sufficient resource ordering control, and that using run stages indiscriminately can make your code very hard to follow. The use of run stages should be kept to a minimum wherever possible. There are a few key examples where the use of stages creates less complexity. The most notable is when a resource modifies the system used to install packages on the system. It helps to have a package management stage that comes before the main stage. When packages are defined in the main (default) stage, your manifests can count on the updated package management configuration information being present. For instance, for a Yum-based system, you would create a yumrepos stage that comes before main. You can specify this dependency using chaining arrows as shown in the following code snippet:

  stage {'yumrepos': }
  Stage['yumrepos'] -> Stage['main']

We can then create a class that creates a Yum repository (yumrepo) resource and assign it to the yumrepos stage as follows:

  class {'yums': stage => 'yumrepos',
  }
  class yums {
    notify {'always before the rest': }
    yumrepo {'testrepo': baseurl => 'file:///var/yum', ensure  => 'present',
    }
  }

For Apt-based systems, the same example would be a stage where Apt sources are defined. The key with stages is to keep their definitions in your site.pp file where they are highly visible and to only use them sparingly where you can guarantee that you will not introduce dependency cycles.

See also

  • The Using tags recipe, in this chapter
  • The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
How to do it…

Here are the steps to create an example of using run stages:

Create the file modules/admin/manifests/stages.pp with the following contents:
  class admin::stages {
    stage { 'first': before => Stage['main'] }
    stage { 'last': require => Stage['main'] }
    class me_first {
      notify { 'This will be done first': }
    }
    class me_last {
      notify { 'This will be done last': }
    }
    class { 'me_first':
      stage => 'first',
    }
    class { 'me_last':
      stage => 'last',
    }
  }
Modify your site.pp file as follows:
  node 'cookbook' {
    class {'first_class': }
    class {'second_class': }
    include admin::stages
  }
Run
  1. Puppet:
    root@cookbook:~# puppet agent -t
    Info: Applying configuration version '1411019225'
    Notice: This will be done first
    Notice: Second Class
    Notice: First Class
    Notice: This will be done last
    Notice: Finished catalog run in 0.43 seconds
    

How it works…

Let's examine this code in detail to see what's happening. First, we declare the run stages first and last, as follows:

  stage { 'first': before => Stage['main'] }
  stage { 'last': require => Stage['main'] }

For the first stage, we've specified that it should come before main. That is, every resource marked as being in the first stage will be applied before any resource in the main stage (the default stage).

The last stage requires the main stage, so no resource in the last stage can be applied until after every resource in the main stage.

We then declare some classes that we'll later assign to these run stages:

  class me_first {
    notify { 'This will be done first': }
  }
  class me_last {
    notify { 'This will be done last': }
  }

We can now put it all together and include these classes on the node, specifying the run stages for each as we do so:

  class { 'me_first': stage => 'first',
  }
  class { 'me_last': stage => 'last',
  }

Note that in the class declarations for me_first and me_last, we didn't have to specify that they take a stage parameter. The stage parameter is another metaparameter, which means it can be applied to any class or resource without having to be explicitly declared. When we ran puppet agent on our Puppet node, the notify from the me_first class was applied before the notifies from first_class and second_class. The notify from me_last was applied after the main stage, so it comes after the two notifies from first_class and second_class. If you run puppet agent multiple times, you will see that the notifies from first_class and second_class may not always appear in the same order but the me_first class will always come first and the me_last class will always come last.

There's more…

You can define as many run stages as you like, and set up any ordering for them. This can greatly simplify a complicated manifest that would otherwise require lots of explicit dependencies between resources. Beware of accidentally introducing dependency cycles, though; when you assign something to a run stage you're automatically making it dependent on everything in prior stages.

You may like to define your stages in the site.pp file instead, so that at the top level of the manifest, it's easy to see what stages are available.

Gary Larizza has written a helpful introduction to using run stages, with some real-world examples, on his website:

http://garylarizza.com/blog/2011/03/11/using-run-stages-with-puppet/

A caveat: many people don't like to use run stages, feeling that Puppet already provides sufficient resource ordering control, and that using run stages indiscriminately can make your code very hard to follow. The use of run stages should be kept to a minimum wherever possible. There are a few key examples where the use of stages creates less complexity. The most notable is when a resource modifies the system used to install packages on the system. It helps to have a package management stage that comes before the main stage. When packages are defined in the main (default) stage, your manifests can count on the updated package management configuration information being present. For instance, for a Yum-based system, you would create a yumrepos stage that comes before main. You can specify this dependency using chaining arrows as shown in the following code snippet:

  stage {'yumrepos': }
  Stage['yumrepos'] -> Stage['main']

We can then create a class that creates a Yum repository (yumrepo) resource and assign it to the yumrepos stage as follows:

  class {'yums': stage => 'yumrepos',
  }
  class yums {
    notify {'always before the rest': }
    yumrepo {'testrepo': baseurl => 'file:///var/yum', ensure  => 'present',
    }
  }

For Apt-based systems, the same example would be a stage where Apt sources are defined. The key with stages is to keep their definitions in your site.pp file where they are highly visible and to only use them sparingly where you can guarantee that you will not introduce dependency cycles.

See also

  • The Using tags recipe, in this chapter
  • The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
How it works…

Let's examine this code in detail to see what's happening. First, we declare the run stages first and last, as follows:

stage { 'first': before => Stage['main'] } stage { 'last': require => Stage['main'] }

For the first stage, we've specified that it should come before main. That is, every resource marked as being in the first stage will be applied before any resource in the main stage (the default stage).

The last stage requires the main stage, so no resource in the last stage can be applied until after every resource in the main stage.

We then declare some classes that we'll later assign to these run stages:

class me_first { notify { 'This will be done first': } } class me_last { notify { 'This will be done last': } }

We can now put it

all together and include these classes on the node, specifying the run stages for each as we do so:

  class { 'me_first': stage => 'first',
  }
  class { 'me_last': stage => 'last',
  }

Note that in the class declarations for me_first and me_last, we didn't have to specify that they take a stage parameter. The stage parameter is another metaparameter, which means it can be applied to any class or resource without having to be explicitly declared. When we ran puppet agent on our Puppet node, the notify from the me_first class was applied before the notifies from first_class and second_class. The notify from me_last was applied after the main stage, so it comes after the two notifies from first_class and second_class. If you run puppet agent multiple times, you will see that the notifies from first_class and second_class may not always appear in the same order but the me_first class will always come first and the me_last class will always come last.

There's more…

You can define as many run stages as you like, and set up any ordering for them. This can greatly simplify a complicated manifest that would otherwise require lots of explicit dependencies between resources. Beware of accidentally introducing dependency cycles, though; when you assign something to a run stage you're automatically making it dependent on everything in prior stages.

You may like to define your stages in the site.pp file instead, so that at the top level of the manifest, it's easy to see what stages are available.

Gary Larizza has written a helpful introduction to using run stages, with some real-world examples, on his website:

http://garylarizza.com/blog/2011/03/11/using-run-stages-with-puppet/

A caveat: many people don't like to use run stages, feeling that Puppet already provides sufficient resource ordering control, and that using run stages indiscriminately can make your code very hard to follow. The use of run stages should be kept to a minimum wherever possible. There are a few key examples where the use of stages creates less complexity. The most notable is when a resource modifies the system used to install packages on the system. It helps to have a package management stage that comes before the main stage. When packages are defined in the main (default) stage, your manifests can count on the updated package management configuration information being present. For instance, for a Yum-based system, you would create a yumrepos stage that comes before main. You can specify this dependency using chaining arrows as shown in the following code snippet:

  stage {'yumrepos': }
  Stage['yumrepos'] -> Stage['main']

We can then create a class that creates a Yum repository (yumrepo) resource and assign it to the yumrepos stage as follows:

  class {'yums': stage => 'yumrepos',
  }
  class yums {
    notify {'always before the rest': }
    yumrepo {'testrepo': baseurl => 'file:///var/yum', ensure  => 'present',
    }
  }

For Apt-based systems, the same example would be a stage where Apt sources are defined. The key with stages is to keep their definitions in your site.pp file where they are highly visible and to only use them sparingly where you can guarantee that you will not introduce dependency cycles.

See also

  • The Using tags recipe, in this chapter
  • The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
There's more…

You can define as many run stages as you like, and set up any ordering for them. This can greatly simplify a complicated manifest that would otherwise require lots of explicit dependencies between resources. Beware of accidentally introducing dependency cycles, though; when you assign something to a run stage you're automatically making it dependent on everything in prior stages.

You may like to define your stages in the site.pp file instead, so that at the top level of the manifest, it's easy to see what stages are available.

Gary Larizza has written a helpful introduction to using run stages, with

some real-world examples, on his website:

http://garylarizza.com/blog/2011/03/11/using-run-stages-with-puppet/

A caveat: many people don't like to use run stages, feeling that Puppet already provides sufficient resource ordering control, and that using run stages indiscriminately can make your code very hard to follow. The use of run stages should be kept to a minimum wherever possible. There are a few key examples where the use of stages creates less complexity. The most notable is when a resource modifies the system used to install packages on the system. It helps to have a package management stage that comes before the main stage. When packages are defined in the main (default) stage, your manifests can count on the updated package management configuration information being present. For instance, for a Yum-based system, you would create a yumrepos stage that comes before main. You can specify this dependency using chaining arrows as shown in the following code snippet:

  stage {'yumrepos': }
  Stage['yumrepos'] -> Stage['main']

We can then create a class that creates a Yum repository (yumrepo) resource and assign it to the yumrepos stage as follows:

  class {'yums': stage => 'yumrepos',
  }
  class yums {
    notify {'always before the rest': }
    yumrepo {'testrepo': baseurl => 'file:///var/yum', ensure  => 'present',
    }
  }

For Apt-based systems, the same example would be a stage where Apt sources are defined. The key with stages is to keep their definitions in your site.pp file where they are highly visible and to only use them sparingly where you can guarantee that you will not introduce dependency cycles.

See also

  • The Using tags recipe, in this chapter
  • The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
See also

The Using tags recipe, in this chapter
The Drawing dependency graphs recipe in
  • Chapter 10, Monitoring, Reporting, and Troubleshooting

Using roles and profiles

Well organized Puppet manifests are easy to read; the purpose of a module should be evident in its name. The purpose of a node should be defined in a single class. This single class should include all classes that are required to perform that purpose. Craig Dunn wrote a post about such a classification system, which he dubbed "roles and profiles" (http://www.craigdunn.org/2012/05/239/). In this model, roles are the single purpose of a node, a node may only have one role, a role may contain more than one profile, and a profile contains all the resources related to a single service. In this example, we will create a web server role that uses several profiles.

How to do it…

We'll create two modules to store our roles and profiles. Roles will contain one or more profiles. Each role or profile will be defined as a subclass, such as profile::base

  1. Decide on a naming strategy for your roles and profiles. In our example, we will create two modules, roles and profiles that will contain our roles and profiles respectively:
    $ puppet module generate thomas-profiles
    $ ln -s thomas-profiles profiles
    $ puppet module generate thomas-roles
    $ ln -s thomas-roles roles
    
  2. Begin defining the constituent parts of our webserver role as profiles. To keep this example simple, we will create two profiles. First, a base profile to include our basic server configuration classes. Second, an apache class to install and configure the apache web server (httpd) as follows:
    $ vim profiles/manifests/base.pp
    class profiles::base {
      include base
    }
    $ vim profiles/manifests/apache.pp
    class profiles::apache {
      $apache = $::osfamily ? {
        'RedHat' => 'httpd',
        'Debian' => 'apache2',
        }
      service { "$apache":
        enable => true,
        ensure => true,
      }
      package { "$apache":
        ensure => 'installed',
      }
    }
    
  3. Define a roles::webserver class for our webserver role as follows:
    $ vim roles/manifests/webserver.pp
    class roles::webserver {
      include profiles::apache
      include profiles::base
    }
    
  4. Apply the roles::webserver class to a node. In a centralized installation, you would use either an External Node Classifier (ENC) to apply the class to the node, or you would use Hiera to define the role:
      node 'webtest' {
        include roles::webserver
      }

How it works…

Breaking down the parts of the web server configuration into different profiles allows us to apply those parts independently. We created a base profile that we can expand to include all the resources we would like applied to all nodes. Our roles::webserver class simply includes the base and apache classes.

There's more…

As we'll see in the next section, we can pass parameters to classes to alter how they work. In our roles::webserver class, we can use the class instantiation syntax instead of include, and override it with parameters in the classes. For instance, to pass a parameter to the base class, we would use:

  class {'profiles::base':
    parameter => 'newvalue'
  }

where we previously used:

include profiles::base

Tip

In previous versions of this book, node and class inheritance were used to achieve a similar goal, code reuse. Node inheritance is deprecated in Puppet Version 3.7 and higher. Node and class inheritance should be avoided. Using roles and profiles achieves the same level of readability and is much easier to follow.

How to do it…

We'll create two modules to store our roles and profiles. Roles will contain one or more profiles. Each role or profile will be defined as a subclass, such as profile::base

Decide on a naming strategy for your roles and profiles. In our example, we will create two modules, roles and profiles that will contain our roles and profiles respectively:
$ puppet module generate thomas-profiles
$ ln -s thomas-profiles profiles
$ puppet module generate thomas-roles
$ ln -s thomas-roles roles
Begin defining the constituent parts of our webserver role as profiles. To keep this example simple, we will create two profiles. First, a base profile to include our basic server configuration classes. Second, an apache class to install and configure the apache web server (httpd) as follows:
$ vim profiles/manifests/base.pp
class profiles::base {
  include base
}
$ vim profiles/manifests/apache.pp
class profiles::apache {
  $apache = $::osfamily ? {
    'RedHat' => 'httpd',
    'Debian' => 'apache2',
    }
  service { "$apache":
    enable => true,
    ensure => true,
  }
  package { "$apache":
    ensure => 'installed',
  }
}
Define a roles::webserver class for our webserver role as follows:
$ vim roles/manifests/webserver.pp
class roles::webserver {
  include profiles::apache
  include profiles::base
}
Apply the roles::webserver class to a node. In a centralized installation, you would use either an External Node Classifier (ENC)
  1. to apply the class to the node, or you would use Hiera to define the role:
      node 'webtest' {
        include roles::webserver
      }

How it works…

Breaking down the parts of the web server configuration into different profiles allows us to apply those parts independently. We created a base profile that we can expand to include all the resources we would like applied to all nodes. Our roles::webserver class simply includes the base and apache classes.

There's more…

As we'll see in the next section, we can pass parameters to classes to alter how they work. In our roles::webserver class, we can use the class instantiation syntax instead of include, and override it with parameters in the classes. For instance, to pass a parameter to the base class, we would use:

  class {'profiles::base':
    parameter => 'newvalue'
  }

where we previously used:

include profiles::base

Tip

In previous versions of this book, node and class inheritance were used to achieve a similar goal, code reuse. Node inheritance is deprecated in Puppet Version 3.7 and higher. Node and class inheritance should be avoided. Using roles and profiles achieves the same level of readability and is much easier to follow.

How it works…

Breaking

down the parts of the web server configuration into different profiles allows us to apply those parts independently. We created a base profile that we can expand to include all the resources we would like applied to all nodes. Our roles::webserver class simply includes the base and apache classes.

There's more…

As we'll see in the next section, we can pass parameters to classes to alter how they work. In our roles::webserver class, we can use the class instantiation syntax instead of include, and override it with parameters in the classes. For instance, to pass a parameter to the base class, we would use:

  class {'profiles::base':
    parameter => 'newvalue'
  }

where we previously used:

include profiles::base

Tip

In previous versions of this book, node and class inheritance were used to achieve a similar goal, code reuse. Node inheritance is deprecated in Puppet Version 3.7 and higher. Node and class inheritance should be avoided. Using roles and profiles achieves the same level of readability and is much easier to follow.

There's more…

As we'll see in the next section, we can pass parameters to classes to alter how they work. In our roles::webserver class, we can use the class instantiation syntax instead of include, and override it with parameters in the classes. For instance, to pass a parameter to the base class, we would use:

class {'profiles::base': parameter => 'newvalue' }

where we previously used:

include profiles::base

Tip

In previous versions of this book, node and class inheritance were used to achieve a similar goal, code reuse. Node inheritance is deprecated in Puppet Version 3.7 and higher. Node and class inheritance should be avoided. Using roles and profiles achieves the same level of readability and is much easier to follow.

Passing parameters to classes

Sometimes it's very useful to parameterize some aspect of a class. For example, you might need to manage different versions of a gem package, and rather than making separate classes for each that differ only in the version number, you can pass in the version number as a parameter.

How to do it…

In this example, we'll create a definition that accepts parameters:

  1. Declare the parameter as a part of the class definition:
      class eventmachine($version) {
        package { 'eventmachine': provider => gem, ensure   => $version,
        }
      }
  2. Use the following syntax to include the class on a node:
      class { 'eventmachine':
        version => '1.0.3',
      }

How it works…

The class definition class eventmachine($version) { is just like a normal class definition except it specifies that the class takes one parameter: $version. Inside the class, we've defined a package resource:

  package { 'eventmachine':
    provider => gem,
    ensure   => $version,
  }

This is a gem package, and we're requesting to install version $version.

Include the class on a node, instead of the usual include syntax:

include eventmachine

On doing so, there will be a class statement:

  class { 'eventmachine':
    version => '1.0.3',
  }

This has the same effect but also sets a value for the parameter as version.

There's more…

You can specify multiple parameters for a class as:

  class mysql($package, $socket, $port) {

Then supply them in the same way:

  class { 'mysql':
    package => 'percona-server-server-5.5',
    socket  => '/var/run/mysqld/mysqld.sock',
    port    => '3306',
  }

Specifying default values

You can also give default values for some of your parameters. When you include the class without setting a parameter, the default value will be used. For instance, if we created a mysql class with three parameters, we could provide default values for any or all of the parameters as shown in the code snippet:

class mysql($package, $socket, $port='3306') {

or all:

  class mysql(
    package = percona-server-server-5.5",
    socket  = '/var/run/mysqld/mysqld.sock',
    port    = '3306') {

Defaults allow you to use a default value and override that default where you need it.

Unlike a definition, only one instance of a parameterized class can exist on a node. So where you need to have several different instances of the resource, use define instead.

How to do it…

In this example, we'll create a definition that accepts parameters:

Declare the parameter as a part of the class definition:
  class eventmachine($version) {
    package { 'eventmachine': provider => gem, ensure   => $version,
    }
  }
Use the following syntax to include the class on a node:
  class { 'eventmachine':
    version => '1.0.3',
  }

How it works…

The class definition class eventmachine($version) { is just like a normal class definition except it specifies that the class takes one parameter: $version. Inside the class, we've defined a package resource:

  package { 'eventmachine':
    provider => gem,
    ensure   => $version,
  }

This is a gem package, and we're requesting to install version $version.

Include the class on a node, instead of the usual include syntax:

include eventmachine

On doing so, there will be a class statement:

  class { 'eventmachine':
    version => '1.0.3',
  }

This has the same effect but also sets a value for the parameter as version.

There's more…

You can specify multiple parameters for a class as:

  class mysql($package, $socket, $port) {

Then supply them in the same way:

  class { 'mysql':
    package => 'percona-server-server-5.5',
    socket  => '/var/run/mysqld/mysqld.sock',
    port    => '3306',
  }

Specifying default values

You can also give default values for some of your parameters. When you include the class without setting a parameter, the default value will be used. For instance, if we created a mysql class with three parameters, we could provide default values for any or all of the parameters as shown in the code snippet:

class mysql($package, $socket, $port='3306') {

or all:

  class mysql(
    package = percona-server-server-5.5",
    socket  = '/var/run/mysqld/mysqld.sock',
    port    = '3306') {

Defaults allow you to use a default value and override that default where you need it.

Unlike a definition, only one instance of a parameterized class can exist on a node. So where you need to have several different instances of the resource, use define instead.

How it works…

The class definition class eventmachine($version) { is just like a normal class definition except it specifies that the class takes one parameter: $version. Inside the class, we've defined a package resource:

package { 'eventmachine': provider => gem, ensure => $version, }

This is a gem package, and we're requesting to install version $version.

Include the class on a node, instead of the usual include syntax:

include eventmachine

On doing so, there will be a class statement:

class { 'eventmachine': version => '1.0.3', }

This has the

same effect but also sets a value for the parameter as version.

There's more…

You can specify multiple parameters for a class as:

  class mysql($package, $socket, $port) {

Then supply them in the same way:

  class { 'mysql':
    package => 'percona-server-server-5.5',
    socket  => '/var/run/mysqld/mysqld.sock',
    port    => '3306',
  }

Specifying default values

You can also give default values for some of your parameters. When you include the class without setting a parameter, the default value will be used. For instance, if we created a mysql class with three parameters, we could provide default values for any or all of the parameters as shown in the code snippet:

class mysql($package, $socket, $port='3306') {

or all:

  class mysql(
    package = percona-server-server-5.5",
    socket  = '/var/run/mysqld/mysqld.sock',
    port    = '3306') {

Defaults allow you to use a default value and override that default where you need it.

Unlike a definition, only one instance of a parameterized class can exist on a node. So where you need to have several different instances of the resource, use define instead.

There's more…

You can specify multiple parameters for a class as:

class mysql($package, $socket, $port) {

Then supply them in the same way:

class { 'mysql': package => 'percona-server-server-5.5', socket => '/var/run/mysqld/mysqld.sock', port => '3306', }

Specifying default values

You can also give default values for some of your parameters. When you include the class without setting a parameter, the default value will be used. For instance, if we created a mysql class with three parameters, we could provide default values for any or all of the parameters as shown in the code snippet:

class mysql($package, $socket, $port='3306') {

or all:

  class mysql(
    package = percona-server-server-5.5",
    socket  = '/var/run/mysqld/mysqld.sock',
    port    = '3306') {

Defaults allow you to use a default value and override that default where you need it.

Unlike a definition, only one instance of a parameterized class can exist on a node. So where you need to have several different instances of the resource, use define instead.

Specifying default values

You can also give default values for some of your

parameters. When you include the class without setting a parameter, the default value will be used. For instance, if we created a mysql class with three parameters, we could provide default values for any or all of the parameters as shown in the code snippet:

class mysql($package, $socket, $port='3306') {

or all:

  class mysql(
    package = percona-server-server-5.5",
    socket  = '/var/run/mysqld/mysqld.sock',
    port    = '3306') {

Defaults allow you to use a default value and override that default where you need it.

Unlike a definition, only one instance of a parameterized class can exist on a node. So where you need to have several different instances of the resource, use define instead.

Passing parameters from Hiera

Like the parameter defaults we introduced in the previous chapter, Hiera may be used to provide default values to classes. This feature requires Puppet Version 3 and higher.

Getting ready

Install and configure hiera as we did in Chapter 2, Puppet Infrastructure. Create a global or common yaml file; this will serve as the default for all values.

How to do it...

  1. Create a class with parameters and no default values:
    t@mylaptop ~/puppet $ mkdir -p modules/mysql/manifests t@mylaptop ~/puppet $ vim modules/mysql/manifests/init.pp
    class mysql ( $port, $socket, $package ) {
      notify {"Port: $port Socket: $socket Package: $package": }
    }
    
  2. Update your common .yaml file in Hiera with the default values for the mysql class:
    ---
    mysql::port: 3306
    mysql::package: 'mysql-server'
    mysql::socket: '/var/lib/mysql/mysql.sock'
    

    Apply the class to a node, you can add the mysql class to your default node for now.

    node default {
      class {'mysql': }
    }
    
  3. Run puppet agent and verify the output:
    [root@hiera-test ~]# puppet agent -t
    Info: Caching catalog for hiera-test.example.com
    Info: Applying configuration version '1411182251'
    Notice: Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server
    Notice: /Stage[main]/Mysql/Notify[Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server]/message: defined 'message' as 'Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server'
    Notice: Finished catalog run in 1.75 seconds
    

How it works...

When we instantiate the mysql class in our manifest, we provided no values for any of the attributes. Puppet knows to look for a value in Hiera that matches class_name::parameter_name: or ::class_name::parameter_name:.

When Puppet finds a value, it uses it as the parameter for the class. If Puppet fails to find a value in Hiera and no default is defined, a catalog failure will result in the following command line:

Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Must pass package to Class[Mysql] at /etc/puppet/environments/production/manifests/site.pp:6 on node hiera-test.example.com

This error indicates that Puppet would like a value for the parameter package.

There's more...

You can define a Hiera hierarchy and supply different values for parameters based on facts. You could, for instance, have %{::osfamily} in your hierarchy and have different yaml files based on the osfamily parameter (RedHat, Suse, and Debian).

Getting ready

Install and configure hiera as we did in

Chapter 2, Puppet Infrastructure. Create a global or common yaml file; this will serve as the default for all values.

How to do it...

  1. Create a class with parameters and no default values:
    t@mylaptop ~/puppet $ mkdir -p modules/mysql/manifests t@mylaptop ~/puppet $ vim modules/mysql/manifests/init.pp
    class mysql ( $port, $socket, $package ) {
      notify {"Port: $port Socket: $socket Package: $package": }
    }
    
  2. Update your common .yaml file in Hiera with the default values for the mysql class:
    ---
    mysql::port: 3306
    mysql::package: 'mysql-server'
    mysql::socket: '/var/lib/mysql/mysql.sock'
    

    Apply the class to a node, you can add the mysql class to your default node for now.

    node default {
      class {'mysql': }
    }
    
  3. Run puppet agent and verify the output:
    [root@hiera-test ~]# puppet agent -t
    Info: Caching catalog for hiera-test.example.com
    Info: Applying configuration version '1411182251'
    Notice: Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server
    Notice: /Stage[main]/Mysql/Notify[Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server]/message: defined 'message' as 'Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server'
    Notice: Finished catalog run in 1.75 seconds
    

How it works...

When we instantiate the mysql class in our manifest, we provided no values for any of the attributes. Puppet knows to look for a value in Hiera that matches class_name::parameter_name: or ::class_name::parameter_name:.

When Puppet finds a value, it uses it as the parameter for the class. If Puppet fails to find a value in Hiera and no default is defined, a catalog failure will result in the following command line:

Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Must pass package to Class[Mysql] at /etc/puppet/environments/production/manifests/site.pp:6 on node hiera-test.example.com

This error indicates that Puppet would like a value for the parameter package.

There's more...

You can define a Hiera hierarchy and supply different values for parameters based on facts. You could, for instance, have %{::osfamily} in your hierarchy and have different yaml files based on the osfamily parameter (RedHat, Suse, and Debian).

How to do it...

Create a class with parameters and no default values:
t@mylaptop ~/puppet $ mkdir -p modules/mysql/manifests t@mylaptop ~/puppet $ vim modules/mysql/manifests/init.pp
class mysql ( $port, $socket, $package ) {
  notify {"Port: $port Socket: $socket Package: $package": }
}
Update your common .yaml file in Hiera with the default values for the mysql class:
---
mysql::port: 3306
mysql::package: 'mysql-server'
mysql::socket: '/var/lib/mysql/mysql.sock'

Apply the class to a node, you can add the mysql class to your default node for now.

node default {
  class {'mysql': }
}
Run puppet agent and verify the output:
[root@hiera-test ~]# puppet agent -t
Info: Caching catalog for hiera-test.example.com
Info: Applying configuration version '1411182251'
Notice: Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server
Notice: /Stage[main]/Mysql/Notify[Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server]/message: defined 'message' as 'Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server'
Notice: Finished catalog run in 1.75 seconds

How it works...

When we instantiate the mysql class in our manifest, we provided no values for any of the attributes. Puppet knows to look for a value in Hiera that matches class_name::parameter_name: or ::class_name::parameter_name:.

When Puppet finds a value, it uses it as the parameter for the class. If Puppet fails to find a value in Hiera and no default is defined, a catalog failure will result in the following command line:

Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Must pass package to Class[Mysql] at /etc/puppet/environments/production/manifests/site.pp:6 on node hiera-test.example.com

This error indicates that Puppet would like a value for the parameter package.

There's more...

You can define a Hiera hierarchy and supply different values for parameters based on facts. You could, for instance, have %{::osfamily} in your hierarchy and have different yaml files based on the osfamily parameter (RedHat, Suse, and Debian).

How it works...

When we instantiate the mysql class in our manifest, we provided no values for any of the attributes. Puppet knows to look for a value in Hiera that matches class_name::parameter_name: or ::class_name::parameter_name:.

When Puppet finds a

value, it uses it as the parameter for the class. If Puppet fails to find a value in Hiera and no default is defined, a catalog failure will result in the following command line:

Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Must pass package to Class[Mysql] at /etc/puppet/environments/production/manifests/site.pp:6 on node hiera-test.example.com

This error indicates that Puppet would like a value for the parameter package.

There's more...

You can define a Hiera hierarchy and supply different values for parameters based on facts. You could, for instance, have %{::osfamily} in your hierarchy and have different yaml files based on the osfamily parameter (RedHat, Suse, and Debian).

There's more...

You can define a Hiera hierarchy and supply different values for parameters based on facts. You could, for instance, have %{::osfamily} in your hierarchy and have different yaml files based on the osfamily parameter (RedHat, Suse, and Debian).

Writing reusable, cross-platform manifests

Every system administrator dreams of a unified, homogeneous infrastructure of identical machines all running the same version of the same OS. As in other areas of life, however, the reality is often messy and doesn't conform to the plan.

You are probably responsible for a bunch of assorted servers of varying age and architecture running different kernels from different OS distributions, often scattered across different data centers and ISPs.

This situation should strike terror into the hearts of the sysadmins of the SSH in a for loop persuasion, because executing the same commands on every server can have different, unpredictable, and even dangerous results.

We should certainly strive to bring older servers up to date and get working as far as possible on a single reference platform to make administration simpler, cheaper, and more reliable. But until we get there, Puppet makes coping with heterogeneous environments slightly easier.

How to do it…

Here are some examples of how to make your manifests more portable:

  1. Where you need to apply the same manifest to servers with different OS distributions, the main differences will probably be the names of packages and services, and the location of config files. Try to capture all these differences into a single class by using selectors to set global variables:
      $ssh_service = $::operatingsystem? { /Ubuntu|Debian/ => 'ssh', default         => 'sshd',
      }

    You needn't worry about the differences in any other part of the manifest; when you refer to something, use the variable with confidence that it will point to the right thing in each environment:

      service { $ssh_service: ensure => running,
      }
  2. Often we need to cope with mixed architectures; this can affect the paths to shared libraries, and also may require different versions of packages. Again, try to encapsulate all the required settings in a single architecture class that sets global variables:
      $libdir = $::architecture ? {
        /amd64|x86_64/   => '/usr/lib64', default => '/usr/lib',
      }

    Then you can use these wherever an architecture-dependent value is required in your manifests or even in templates:

    ; php.ini
    [PHP]
    ; Directory in which the loadable extensions (modules) reside.
    extension_dir = <%= @libdir %>/php/modules

How it works...

The advantage of this approach (which could be called top-down) is that you only need to make your choices once. The alternative, bottom-up approach would be to have a selector or case statement everywhere a setting is used:

  service { $::operatingsystem? {
    /Ubuntu|Debian/ => 'ssh', default         => 'sshd' }: ensure => running,
  }

This not only results in lots of duplication, but makes the code harder to read. And when a new operating system is added to the mix, you'll need to make changes throughout the whole manifest, instead of just in one place.

There's more…

If you are writing a module for public distribution (for example, on Puppet Forge), making your module as cross-platform as possible will make it more valuable to the community. As far as you can, test it on many different distributions, platforms, and architectures, and add the appropriate variables so that it works everywhere.

If you use a public module and adapt it to your own environment, consider updating the public version with your changes if you think they might be helpful to other people.

Even if you are not thinking of publishing a module, bear in mind that it may be in production use for a long time and may have to adapt to many changes in the environment. If it's designed to cope with this from the start, it'll make life easier for you or whoever ends up maintaining your code.

 

"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."

 
 --Dave Carhart

See also

  • The Using public modules recipe in Chapter 7, Managing Applications
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
How to do it…

Here are some examples of how to make your manifests more portable:

Where you need to apply the same manifest to servers with different OS distributions, the main differences will probably be the names of packages and services, and the location of config files. Try to capture all these differences into a single class by using selectors to set global variables:
  $ssh_service = $::operatingsystem? { /Ubuntu|Debian/ => 'ssh', default         => 'sshd',
  }

You needn't worry about the differences in any other part of the manifest; when you refer to something, use the variable with confidence that it will point to the right thing in each environment:

  service { $ssh_service: ensure => running,
  }
Often we need to cope with mixed architectures; this can affect the paths to shared libraries, and also may require different versions of packages. Again, try to encapsulate all the required settings in a single architecture class that sets global variables:
  $libdir = $::architecture ? {
    /amd64|x86_64/   => '/usr/lib64', default => '/usr/lib',
  }
Then you can use
  1. these wherever an architecture-dependent value is required in your manifests or even in templates:

    ; php.ini
    [PHP]
    ; Directory in which the loadable extensions (modules) reside.
    extension_dir = <%= @libdir %>/php/modules

How it works...

The advantage of this approach (which could be called top-down) is that you only need to make your choices once. The alternative, bottom-up approach would be to have a selector or case statement everywhere a setting is used:

  service { $::operatingsystem? {
    /Ubuntu|Debian/ => 'ssh', default         => 'sshd' }: ensure => running,
  }

This not only results in lots of duplication, but makes the code harder to read. And when a new operating system is added to the mix, you'll need to make changes throughout the whole manifest, instead of just in one place.

There's more…

If you are writing a module for public distribution (for example, on Puppet Forge), making your module as cross-platform as possible will make it more valuable to the community. As far as you can, test it on many different distributions, platforms, and architectures, and add the appropriate variables so that it works everywhere.

If you use a public module and adapt it to your own environment, consider updating the public version with your changes if you think they might be helpful to other people.

Even if you are not thinking of publishing a module, bear in mind that it may be in production use for a long time and may have to adapt to many changes in the environment. If it's designed to cope with this from the start, it'll make life easier for you or whoever ends up maintaining your code.

 

"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."

 
 --Dave Carhart

See also

  • The Using public modules recipe in Chapter 7, Managing Applications
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
How it works...

The advantage of this approach (which could be called top-down) is that you only need to make your choices once. The alternative, bottom-up approach would be to have a selector or case statement everywhere a setting is used:

service { $::operatingsystem? { /Ubuntu|Debian/ => 'ssh', default => 'sshd' }: ensure => running, }

This not only results in lots of

duplication, but makes the code harder to read. And when a new operating system is added to the mix, you'll need to make changes throughout the whole manifest, instead of just in one place.

There's more…

If you are writing a module for public distribution (for example, on Puppet Forge), making your module as cross-platform as possible will make it more valuable to the community. As far as you can, test it on many different distributions, platforms, and architectures, and add the appropriate variables so that it works everywhere.

If you use a public module and adapt it to your own environment, consider updating the public version with your changes if you think they might be helpful to other people.

Even if you are not thinking of publishing a module, bear in mind that it may be in production use for a long time and may have to adapt to many changes in the environment. If it's designed to cope with this from the start, it'll make life easier for you or whoever ends up maintaining your code.

 

"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."

 
 --Dave Carhart

See also

  • The Using public modules recipe in Chapter 7, Managing Applications
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
There's more…

If you are writing a module for

public distribution (for example, on Puppet Forge), making your module as cross-platform as possible will make it more valuable to the community. As far as you can, test it on many different distributions, platforms, and architectures, and add the appropriate variables so that it works everywhere.

If you use a public module and adapt it to your own environment, consider updating the public version with your changes if you think they might be helpful to other people.

Even if you are not thinking of publishing a module, bear in mind that it may be in production use for a long time and may have to adapt to many changes in the environment. If it's designed to cope with this from the start, it'll make life easier for you or whoever ends up maintaining your code.

 

"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."

 
 --Dave Carhart

See also

  • The Using public modules recipe in Chapter 7, Managing Applications
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
See also

The Using public modules recipe in
  • Chapter 7, Managing Applications
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure

Getting information about the environment

Often in a Puppet manifest, you need to know some local information about the machine you're on. Facter is the tool that accompanies Puppet to provide a standard way of getting information (facts) from the environment about things such as these:

  • Operating system
  • Memory size
  • Architecture
  • Processor count

To see a complete list of the facts available on your system, run:

$ sudo facter
architecture => amd64
augeasversion => 0.10.0
domain => compute-1.internal
ec2_ami_id => ami-137bcf7a
ec2_ami_launch_index => 0

Note

While it can be handy to get this information from the command line, the real power of Facter lies in being able to access these facts in your Puppet manifests.

Some modules define their own facts; to see any facts that have been defined locally, add the -p (pluginsync) option to facter as follows:

$ sudo facter -p

How to do it…

Here's an example of using Facter facts in a manifest:

  1. Reference a Facter fact in your manifest like any other variable. Facts are global variables in Puppet, so they should be prefixed with a double colon (::), as in the following code snippet:
    notify { "This is $::operatingsystem version $::operatingsystemrelease, on $::architecture architecture, kernel version $::kernelversion": }
  2. When Puppet runs, it will fill in the appropriate values for the current node:
    [root@hiera-test ~]# puppet agent -t
    ...
    Info: Applying configuration version '1411275985'Notice: This is RedHat version 6.5, on x86_64 architecture, kernel version 2.6.32
    ...
    Notice: Finished catalog run in 0.40 seconds
    

How it works…

Facter provides a standard way for manifests to get information about the nodes to which they are applied. When you refer to a fact in a manifest, Puppet will query Facter to get the current value and insert it into the manifest. Facter facts are top scope variables.

Tip

Always refer to facts with leading double colons to ensure that you are using the fact and not a local variable:

$::hostname NOT $hostname

There's more…

You can also use facts in ERB templates. For example, you might want to insert the node's hostname into a file, or change a configuration setting for an application based on the memory size of the node. When you use fact names in templates, remember that they don't need a dollar sign because this is Ruby, not Puppet:

$KLogPath <%= case @kernelversion when '2.6.31' then
'/var/run/rsyslog/kmsg' else '/proc/kmsg' end %>

When referring to facts, use the @ syntax. Variables that are defined at the same scope as the function call to template can also be referenced with the @ syntax. Out of scope variables should use the scope function. For example, to reference the mysql::port variable we defined earlier in the mysql modules, use the following:

MySQL Port = <%= scope['::mysql::port'] %>

Applying this template results in the following file:

[root@hiera-test ~]# puppet agent -t
...
Info: Caching catalog for hiera-test.example.com
Notice: /Stage[main]/Erb/File[/tmp/template-test]/ensure: defined content as '{md5}96edacaf9747093f73084252c7ca7e67'
Notice: Finished catalog run in 0.41 seconds [root@hiera-test ~]# cat /tmp/template-test
MySQL Port = 3306

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
How to do it…

Here's an example of using Facter facts in a manifest:

Reference a Facter fact in your manifest like any other variable. Facts are global variables in Puppet, so they should be prefixed with a double colon (::), as in the following code snippet:
notify { "This is $::operatingsystem version $::operatingsystemrelease, on $::architecture architecture, kernel version $::kernelversion": }
When Puppet runs, it will fill in the appropriate values for the current node:
[root@hiera-test ~]# puppet agent -t
...
Info: Applying configuration version '1411275985'Notice: This is RedHat version 6.5, on x86_64 architecture, kernel version 2.6.32
...
Notice: Finished catalog run in 0.40 seconds

How it works…

Facter provides a standard way for manifests to get information about the nodes to which they are applied. When you refer to a fact in a manifest, Puppet will query Facter to get the current value and insert it into the manifest. Facter facts are top scope variables.

Tip

Always refer to facts with leading double colons to ensure that you are using the fact and not a local variable:

$::hostname NOT $hostname

There's more…

You can also use facts in ERB templates. For example, you might want to insert the node's hostname into a file, or change a configuration setting for an application based on the memory size of the node. When you use fact names in templates, remember that they don't need a dollar sign because this is Ruby, not Puppet:

$KLogPath <%= case @kernelversion when '2.6.31' then
'/var/run/rsyslog/kmsg' else '/proc/kmsg' end %>

When referring to facts, use the @ syntax. Variables that are defined at the same scope as the function call to template can also be referenced with the @ syntax. Out of scope variables should use the scope function. For example, to reference the mysql::port variable we defined earlier in the mysql modules, use the following:

MySQL Port = <%= scope['::mysql::port'] %>

Applying this template results in the following file:

[root@hiera-test ~]# puppet agent -t
...
Info: Caching catalog for hiera-test.example.com
Notice: /Stage[main]/Erb/File[/tmp/template-test]/ensure: defined content as '{md5}96edacaf9747093f73084252c7ca7e67'
Notice: Finished catalog run in 0.41 seconds [root@hiera-test ~]# cat /tmp/template-test
MySQL Port = 3306

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
How it works…

Facter provides a standard

way for manifests to get information about the nodes to which they are applied. When you refer to a fact in a manifest, Puppet will query Facter to get the current value and insert it into the manifest. Facter facts are top scope variables.

Tip

Always refer to facts with leading double colons to ensure that you are using the fact and not a local variable:

$::hostname NOT $hostname

There's more…

You can also use facts in ERB templates. For example, you might want to insert the node's hostname into a file, or change a configuration setting for an application based on the memory size of the node. When you use fact names in templates, remember that they don't need a dollar sign because this is Ruby, not Puppet:

$KLogPath <%= case @kernelversion when '2.6.31' then
'/var/run/rsyslog/kmsg' else '/proc/kmsg' end %>

When referring to facts, use the @ syntax. Variables that are defined at the same scope as the function call to template can also be referenced with the @ syntax. Out of scope variables should use the scope function. For example, to reference the mysql::port variable we defined earlier in the mysql modules, use the following:

MySQL Port = <%= scope['::mysql::port'] %>

Applying this template results in the following file:

[root@hiera-test ~]# puppet agent -t
...
Info: Caching catalog for hiera-test.example.com
Notice: /Stage[main]/Erb/File[/tmp/template-test]/ensure: defined content as '{md5}96edacaf9747093f73084252c7ca7e67'
Notice: Finished catalog run in 0.41 seconds [root@hiera-test ~]# cat /tmp/template-test
MySQL Port = 3306

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
There's more…

You can also use facts in

ERB templates. For example, you might want to insert the node's hostname into a file, or change a configuration setting for an application based on the memory size of the node. When you use fact names in templates, remember that they don't need a dollar sign because this is Ruby, not Puppet:

$KLogPath <%= case @kernelversion when '2.6.31' then
'/var/run/rsyslog/kmsg' else '/proc/kmsg' end %>

When referring to facts, use the @ syntax. Variables that are defined at the same scope as the function call to template can also be referenced with the @ syntax. Out of scope variables should use the scope function. For example, to reference the mysql::port variable we defined earlier in the mysql modules, use the following:

MySQL Port = <%= scope['::mysql::port'] %>

Applying this template results in the following file:

[root@hiera-test ~]# puppet agent -t
...
Info: Caching catalog for hiera-test.example.com
Notice: /Stage[main]/Erb/File[/tmp/template-test]/ensure: defined content as '{md5}96edacaf9747093f73084252c7ca7e67'
Notice: Finished catalog run in 0.41 seconds [root@hiera-test ~]# cat /tmp/template-test
MySQL Port = 3306

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
See also

The Creating custom facts recipe in
  • Chapter 9, External Tools and the Puppet Ecosystem

Importing dynamic information

Even though some system administrators like to wall themselves off from the rest of the office using piles of old printers, we all need to exchange information with other departments from time to time. For example, you may want to insert data into your Puppet manifests that is derived from some outside source. The generate function is ideal for this. Functions are executed on the machine compiling the catalog (the master for centralized deployments); an example like that shown here will only work in a masterless configuration.

Getting ready

Follow these steps to prepare to run the example:

  1. Create the script /usr/local/bin/message.rb with the following contents:
    #!/usr/bin/env ruby
    puts "This runs on the master if you are centralized"
    
  2. Make the script executable:
    $ sudo chmod a+x /usr/local/bin/message.rb
    

How to do it…

This example calls the external script we created previously and gets its output:

  1. Create a message.pp manifest containing the following:
    $message = generate('/usr/local/bin/message.rb')
    notify { $message: }
    
  2. Run Puppet:
    $ puppet apply message.pp 
    ...
    Notice: /Stage[main]/Main/Notify[This runs on the master if you are centralized
    ]/message: defined 'message' as 'This runs on the master if you are centralized
    

How it works…

The generate function runs the specified script or program and returns the result, in this case, a cheerful message from Ruby.

This isn't terribly useful as it stands but you get the idea. Anything a script can do, print, fetch, or calculate, for example, the results of a database query, can be brought into your manifest using generate. You can also, of course, run standard UNIX utilities such as cat and grep.

There's more…

If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:

$message = generate('/bin/cat', '/etc/motd')

Puppet will try to protect you from malicious shell calls by restricting the characters you can use in a call to generate, so shell pipes and redirection aren't allowed, for example. The simplest and safest thing to do is to put all your logic into a script and then call that script.

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
Getting ready

Follow these steps to prepare to run the example:

Create the script /usr/local/bin/message.rb with the following contents:
#!/usr/bin/env ruby
puts "This runs on the master if you are centralized"
Make the script executable:
$ sudo chmod a+x /usr/local/bin/message.rb

How to do it…

This example calls the external script we created previously and gets its output:

  1. Create a message.pp manifest containing the following:
    $message = generate('/usr/local/bin/message.rb')
    notify { $message: }
    
  2. Run Puppet:
    $ puppet apply message.pp 
    ...
    Notice: /Stage[main]/Main/Notify[This runs on the master if you are centralized
    ]/message: defined 'message' as 'This runs on the master if you are centralized
    

How it works…

The generate function runs the specified script or program and returns the result, in this case, a cheerful message from Ruby.

This isn't terribly useful as it stands but you get the idea. Anything a script can do, print, fetch, or calculate, for example, the results of a database query, can be brought into your manifest using generate. You can also, of course, run standard UNIX utilities such as cat and grep.

There's more…

If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:

$message = generate('/bin/cat', '/etc/motd')

Puppet will try to protect you from malicious shell calls by restricting the characters you can use in a call to generate, so shell pipes and redirection aren't allowed, for example. The simplest and safest thing to do is to put all your logic into a script and then call that script.

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
How to do it…

This example calls the external script we created previously and gets its output:

Create a message.pp manifest containing the following:
$message = generate('/usr/local/bin/message.rb')
notify { $message: }
Run Puppet:
$ puppet apply message.pp 
...
Notice: /Stage[main]/Main/Notify[This runs on the master if you are centralized
]/message: defined 'message' as 'This runs on the master if you are centralized

How it works…

The generate function runs the specified script or program and returns the result, in this case, a cheerful message from Ruby.

This isn't terribly useful as it stands but you get the idea. Anything a script can do, print, fetch, or calculate, for example, the results of a database query, can be brought into your manifest using generate. You can also, of course, run standard UNIX utilities such as cat and grep.

There's more…

If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:

$message = generate('/bin/cat', '/etc/motd')

Puppet will try to protect you from malicious shell calls by restricting the characters you can use in a call to generate, so shell pipes and redirection aren't allowed, for example. The simplest and safest thing to do is to put all your logic into a script and then call that script.

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
How it works…

The generate function runs the specified script or program and returns the result, in this case, a cheerful message from Ruby.

This isn't terribly useful as it stands but you get the idea. Anything a script can do, print, fetch, or calculate, for example, the results of a database query, can be brought into your manifest using generate. You can also, of course, run standard UNIX utilities such as cat and grep.

There's more…

If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:

$message = generate('/bin/cat', '/etc/motd')

Puppet will try to protect you from malicious shell calls by restricting the characters you can use in a call to generate, so shell pipes and redirection aren't allowed, for example. The simplest and safest thing to do is to put all your logic into a script and then call that script.

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
There's more…

If you need to pass

arguments to the executable called by generate, add them as extra arguments to the function call:

$message = generate('/bin/cat', '/etc/motd')

Puppet will try to protect you from malicious shell calls by restricting the characters you can use in a call to generate, so shell pipes and redirection aren't allowed, for example. The simplest and safest thing to do is to put all your logic into a script and then call that script.

See also

  • The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
See also

The Creating custom facts recipe in
  • Chapter 9, External Tools and the Puppet Ecosystem
  • The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure

Passing arguments to shell commands

If you want to insert values into a command line (to be run by an exec resource, for example), they often need to be quoted, especially if they contain spaces. The shellquote function will take any number of arguments, including arrays, and quote each of the arguments and return them all as a space-separated string that you can pass to commands.

In this example, we would like to set up an exec resource that will rename a file; but both the source and the target name contain spaces, so they need to be correctly quoted in the command line.

How to do it…

Here's an example of using the shellquote function:

  1. Create a shellquote.pp manifest with the following command:
    $source = 'Hello Jerry'
    $target = 'Hello... Newman'
    $argstring = shellquote($source, $target)
    $command = "/bin/mv ${argstring}"
    notify { $command: }
    
  2. Run Puppet:
    $ puppet apply shellquote.pp 
    ...
    Notice: /bin/mv "Hello Jerry" "Hello... Newman"
    Notice: /Stage[main]/Main/Notify[/bin/mv "Hello Jerry" "Hello... Newman"]/message: defined 'message' as '/bin/mv "Hello Jerry" "Hello... Newman"'
    

How it works…

First we define the $source and $target variables, which are the two filenames we want to use in the command line:

$source = 'Hello Jerry'
$target = 'Hello... Newman'

Then we call shellquote to concatenate these variables into a quoted, space-separated string as follows:

$argstring = shellquote($source, $target)

Then we put together the final command line:

$command = "/bin/mv ${argstring}"

The result will be:

/bin/mv "Hello Jerry" "Hello... Newman"

This command line can now be run with an exec resource. What would happen if we didn't use shellquote?

$source = 'Hello Jerry'
$target = 'Hello... Newman'
$command = "/bin/mv ${source} ${target}"
notify { $command: }

Notice: /bin/mv Hello Jerry Hello... Newman

This won't work because mv expects space-separated arguments, so it will interpret this as a request to move three files Hello, Jerry, and Hello... into a directory named Newman, which probably isn't what we want.

How to do it…

Here's an example of using the shellquote function:

Create a shellquote.pp manifest with the following command:
$source = 'Hello Jerry'
$target = 'Hello... Newman'
$argstring = shellquote($source, $target)
$command = "/bin/mv ${argstring}"
notify { $command: }
Run Puppet:
$ puppet apply shellquote.pp 
...
Notice: /bin/mv "Hello Jerry" "Hello... Newman"
Notice: /Stage[main]/Main/Notify[/bin/mv "Hello Jerry" "Hello... Newman"]/message: defined 'message' as '/bin/mv "Hello Jerry" "Hello... Newman"'

How it works…

First we define the $source and $target variables, which are the two filenames we want to use in the command line:

$source = 'Hello Jerry'
$target = 'Hello... Newman'

Then we call shellquote to concatenate these variables into a quoted, space-separated string as follows:

$argstring = shellquote($source, $target)

Then we put together the final command line:

$command = "/bin/mv ${argstring}"

The result will be:

/bin/mv "Hello Jerry" "Hello... Newman"

This command line can now be run with an exec resource. What would happen if we didn't use shellquote?

$source = 'Hello Jerry'
$target = 'Hello... Newman'
$command = "/bin/mv ${source} ${target}"
notify { $command: }

Notice: /bin/mv Hello Jerry Hello... Newman

This won't work because mv expects space-separated arguments, so it will interpret this as a request to move three files Hello, Jerry, and Hello... into a directory named Newman, which probably isn't what we want.

How it works…

First we define the $source and $target variables, which are the two filenames we want to use in the command line:

$source = 'Hello Jerry' $target = 'Hello... Newman'

Then we call shellquote to

concatenate these variables into a quoted, space-separated string as follows:

$argstring = shellquote($source, $target)

Then we put together the final command line:

$command = "/bin/mv ${argstring}"

The result will be:

/bin/mv "Hello Jerry" "Hello... Newman"

This command line can now be run with an exec resource. What would happen if we didn't use shellquote?

$source = 'Hello Jerry'
$target = 'Hello... Newman'
$command = "/bin/mv ${source} ${target}"
notify { $command: }

Notice: /bin/mv Hello Jerry Hello... Newman

This won't work because mv expects space-separated arguments, so it will interpret this as a request to move three files Hello, Jerry, and Hello... into a directory named Newman, which probably isn't what we want.

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