"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
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.
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, }
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.
- The Iterating over multiple items recipe in Chapter 1, Puppet Language and Style
package { 'sudo' : ensure => installed } package { 'unzip' : ensure => installed } package { 'locate' : ensure => installed } package { 'lsof' : ensure => installed } package { 'cron' : ensure => installed } package { 'rubygems' : ensure => installed }
package { [ 'cron', 'locate', 'lsof', 'rubygems', 'sudo', 'unzip' ]: ensure => installed, }
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.
- The Iterating over multiple items recipe in Chapter 1, Puppet Language and Style
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.
- The Iterating over multiple items recipe in Chapter 1, Puppet Language and Style
- Chapter 1, Puppet Language and Style
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:
- Create an apache module and create a resource default for the
File
type:class apache { File { owner => 'apache', group => 'apache', mode => 0644, } }
- 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", }
- 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 [email protected]:/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 [email protected]:repos/puppet.git 832f6a9..d639a86 production -> production
- 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
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:
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.
- Create an apache module and create a resource default for the
File
type:class apache { File { owner => 'apache', group => 'apache', mode => 0644, } }
- 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", }
- 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 [email protected]:/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 [email protected]:repos/puppet.git 832f6a9..d639a86 production -> production
- 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
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:
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.
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:
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.
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:
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.
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.
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
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.
Next, pass values to them when we declare an instance of the resource:
You can declare multiple parameters as a comma-separated list:
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.
define tmpfile() { file { "/tmp/${name}": content => "Hello, world\n", } } tmpfile { ['a', 'b', 'c']: }
[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
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.
Next, pass values to them when we declare an instance of the resource:
You can declare multiple parameters as a comma-separated list:
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.
Next, pass values to them when we declare an instance of the resource:
You can declare multiple parameters as a comma-separated list:
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.
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:
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.
- Add the following code to your
site.pp
file (replacingcookbook
with your machine'shostname
):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 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. - Modify your
site.pp
file as follows:node 'cookbook' { tag('tagging') class {'tag_test': } }
- Add a
tag_test
module with the followinginit.pp
(or be lazy and add the following definition to yoursite.pp
):class tag_test { if tagged('tagging') { notify { 'containing node/class was tagged.': } } }
- 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
- 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 ourcookbook
class with two classes:node cookbook { class {'first_class': } class {'second_class': } } class first_class { notify { 'First Class': } } class second_class { notify {'Second Class': } }
- Now when we run
puppet agent
on thecookbook
node, we see both notifies:root@cookbook:~# puppet agent -t Notice: Second Class Notice: First Class Notice: Finished catalog run in 0.22 seconds
- Now apply the
first_class
andadd --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
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.
tagged
to get this information:
site.pp
file (replacing cookbook
with your machine's hostname
):node 'cookbook' { if tagged('cookbook') { notify { 'tagged cookbook': } } }
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
- Modify your
site.pp
file as follows:node 'cookbook' { tag('tagging') class {'tag_test': } }
- Add a
tag_test
module with the followinginit.pp
(or be lazy and add the following definition to yoursite.pp
):class tag_test { if tagged('tagging') { notify { 'containing node/class was tagged.': } } }
- 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
- 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 ourcookbook
class with two classes:node cookbook { class {'first_class': } class {'second_class': } } class first_class { notify { 'First Class': } } class second_class { notify {'Second Class': } }
- Now when we run
puppet agent
on thecookbook
node, we see both notifies:root@cookbook:~# puppet agent -t Notice: Second Class Notice: First Class Notice: Finished catalog run in 0.22 seconds
- Now apply the
first_class
andadd --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
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.
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.
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.
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.
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 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
We then declare some classes that we'll later assign to these run stages:
We can now put it all together and include these classes on the node, specifying the run stages for each as we do so:
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/
- The Using tags recipe, in this chapter
- The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
stages
:
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', } }
site.pp
file as follows:node 'cookbook' { class {'first_class': } class {'second_class': } include admin::stages }
We then declare some classes that we'll later assign to these run stages:
We can now put it all together and include these classes on the node, specifying the run stages for each as we do so:
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/
- The Using tags recipe, in this chapter
- The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
first
and last
, as follows:
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).
last
stage requires the main
stage, so no resource in the last
stage can be applied until after every resource in the main
stage.
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/
- The Using tags recipe, in this chapter
- The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
site.pp
file instead, so that at the top level of the manifest, it's easy to see what stages are available.
some real-world examples, on his website:
http://garylarizza.com/blog/2011/03/11/using-run-stages-with-puppet/
- The Using tags recipe, in this chapter
- The Drawing dependency graphs recipe in Chapter 10, Monitoring, Reporting, and Troubleshooting
- Chapter 10, Monitoring, Reporting, and Troubleshooting
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.
- Decide on a naming strategy for your roles and profiles. In our example, we will create two modules,
roles
andprofiles
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, abase
profile to include our basic server configuration classes. Second, anapache
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 ourwebserver
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) to apply the class to the node, or you would use Hiera to define the role:node 'webtest' { include roles::webserver }
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.
profile::base
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
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', } }
roles::webserver
class for our webserver
role as follows:$ vim roles/manifests/webserver.pp class roles::webserver { include profiles::apache include profiles::base }
roles::webserver
class to a node. In a centralized installation, you would use either an External Node Classifier (ENC)
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.
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.
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:
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.
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:
On doing so, there will be a class
statement:
This has the same effect but also sets a value for the parameter as version
.
You can specify multiple parameters for a class as:
Then supply them in the same way:
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:
Defaults allow you to use a default value and override that default where you need it.
class eventmachine($version) { package { 'eventmachine': provider => gem, ensure => $version, } }
class { 'eventmachine': version => '1.0.3', }
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:
On doing so, there will be a class
statement:
This has the same effect but also sets a value for the parameter as version
.
You can specify multiple parameters for a class as:
Then supply them in the same way:
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:
Defaults allow you to use a default value and override that default where you need it.
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:
gem
package, and we're requesting to install version $version
.
include
syntax:
class
statement:
You can specify multiple parameters for a class as:
Then supply them in the same way:
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:
Defaults allow you to use a default value and override that default where you need it.
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:
Defaults allow you to use a default value and override that default where you need it.
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:
Defaults allow you to use a default value and override that default where you need it.
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.
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.
- 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 themysql
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
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.
- 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 themysql
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
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": } }
.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': } }
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
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:
.
%{::osfamily}
in your hierarchy and have different yaml
files based on the osfamily
parameter (RedHat, Suse, and Debian).
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.
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 these wherever an architecture-dependent value is required in your manifests or even in templates:
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.
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.
"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live." | ||
--Dave Carhart |
- The Using public modules recipe in Chapter 7, Managing Applications
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
$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, }
$libdir = $::architecture ? { /amd64|x86_64/ => '/usr/lib64', default => '/usr/lib', }
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.
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.
"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live." | ||
--Dave Carhart |
- The Using public modules recipe in Chapter 7, Managing Applications
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
case
statement everywhere a setting is used:
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.
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.
"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live." | ||
--Dave Carhart |
- The Using public modules recipe in Chapter 7, Managing Applications
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live." | ||
--Dave Carhart |
- The Using public modules recipe in Chapter 7, Managing Applications
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
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:
To see a complete list of the facts available on your system, run:
Some modules define their own facts; to see any facts that have been defined locally, add the -p (pluginsync)
option to facter as follows:
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
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.
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:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
::
), as in the following code snippet:notify { "This is $::operatingsystem version $::operatingsystemrelease, on $::architecture architecture, kernel version $::kernelversion": }
[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
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.
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:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
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:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
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:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
- Chapter 9, External Tools and the Puppet Ecosystem
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.
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
If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
/usr/local/bin/message.rb
with the following contents:#!/usr/bin/env ruby puts "This runs on the master if you are centralized"
$ sudo chmod a+x /usr/local/bin/message.rb
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
If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
message.pp
manifest containing the following:$message = generate('/usr/local/bin/message.rb') notify { $message: }
$ 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
If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
generate
function runs the specified script or program and returns the result, in this case, a cheerful message from Ruby.
generate
. You can also, of course, run standard UNIX utilities such as cat
and grep
.
If you need to pass arguments to the executable called by generate, add them as extra arguments to the function call:
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
- The Creating custom facts recipe in Chapter 9, External Tools and the Puppet Ecosystem
- The Configuring Hiera recipe in Chapter 2, Puppet Infrastructure
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.
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"'
shellquote
function:
shellquote.pp
manifest with the following command:$source = 'Hello Jerry' $target = 'Hello... Newman' $argstring = shellquote($source, $target) $command = "/bin/mv ${argstring}" notify { $command: }
$ 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"'
$source
and $target
variables, which are the two filenames we want to use in the command line:
shellquote
to