Authors: Jeffrey McCune James Turnbull
Every time you add a new module or file you will need to add it to Git using thegit add
command and then commit it to store it in the repository. I recommend you add and commit changes regularly to ensure you have sufficiently granular revisions to allow you to easily roll back to an earlier state.
Tip
If you’re interested in Git, we strongly recommend Scott Chacon’s excellent book
Pro Git
– also published by Apress. The book is available in both dead tree form and online athttp://progit.org/book/
. Scott is also one of the lead developers of the Git hosting site, GitHub –http://www.github.com
, where you can find a number of Puppet related modules.
Our simplesudo
module is a good introduction to Puppet, but it only showcased a small number of Puppet’s capabilities. It’s now time to expand our Puppet knowledge and develop some new more advanced modules, starting with one to manage SSH on our hosts. We’ll then create a module to manage Postfix onmail.example.com
, one to manage MySQL on our Solaris host,db.example.com
, another to manage Apache and web sites, and finally one to manage Puppet with Puppet itself.
We’ll also introduce you to some best practices for structuring, writing and managing modules and configuration.
We know that we first need to create an appropriate module structure. We’re going to do this under the/etc/puppet/modules
directory on our Puppet master.
$ cd /etc/puppet/modules
$ mkdir –p ssh/{manifests,templates,files}
$ touch ssh/manifests/init.pp
Next, we create some classes inside theinit.pp
file and some initial resources, as shown in
Listing 2-2
.
Listing 2-2.
The ssh module
class ssh::install {
package { "openssh":
ensure => present,
}
}
class ssh::config {
file { "/etc/ssh/sshd_config":
ensure = > present,
owner => 'root',
group => 'root',
mode => 0600,
source => "puppet:///modules/ssh/sshd_config",
require => Class["ssh::install"],
notify => Class["ssh::service"],
}
}
class ssh::service {
service { "sshd":
ensure => running,
hasstatus => true,
hasrestart => true,
enable => true,
require => Class["ssh::config"],
}
}
class ssh {
include ssh::install, ssh::config, ssh::service
}
We’ve created three classes:ssh
,ssh::install
,ssh::config
, andssh::service
. As we mentioned earlier, modules can be made up multiple classes. We use the::
namespace syntax as a way to create structure and organization in our modules. Thessh
prefix tells Puppet that each class belongs in thessh
module, and the class name is suffixed.
Note
We’d also want to create asshd_config
file in thessh/files/
directory so that ourFile["/etc/ssh/sshd_config"]
resource can serve out that file. The easiest way to do this is to copy an existing functionalsshd_config
file and use that. Later we’ll show you how to create template files that allow you to configure per-host configuration in your files. Without this file Puppet will report an error for this resource.
In
Listing 2-2
, we created a functional structure by dividing the components of the service we’re managing into functional domains: things to be installed, things to be configured and things to be executed or run.
Lastly, we created a class calledssh
(which we need to ensure the module is valid) and used theinclude
function to add all the classes to the module.
Lots of classes with lots of resources in ourinit.pp
file means that the file is going to quickly get cluttered and hard to manage. Thankfully, Puppet has an elegant way to manage these classes rather than clutter theinit.pp
file. Each class, rather than being specified in theinit.pp
file, can be specified in an individual file in the manifests directory, for example in assh/manifests/install.pp
file that would contain thessh::install
class:
class ssh::install {
package { "openssh":
ensure => present,
}
}
When Puppet loads thessh
module, it will search the path for files suffixed with.pp
, look inside them for namespaced classes and automatically import them. Let’s quickly put ourssh::config
andssh::service
classes into separate files:
$ touch ssh/manifests/{config.pp,service.pp}
This leaves ourinit.pp
file containing just thessh
class:
class ssh
include ssh::install, ssh::config, ssh::service
}
Ourssh
module directory structure will now look like:
ssh
ssh/files/sshd_config
ssh/manifests/init.pp
ssh/manifests/install.pp
ssh/manifests/config.pp
ssh/manifests/service.pp
ssh/templates
Neat and simple.
Tip
You can nest classes another layer, likessh::config::client
, and our auto-importing magic will still work by placing this class in thessh/manifests/config/client.pp
file.
Now that we’ve created our structure, let’s look at the classes and resources we’ve created. Let’s start with thessh::install
class containing thePackage["openssh"]
resource, which installs the OpenSSH package.
It looks simple enough, but we’ve already hit a stumbling block – we want to manage SSH on all of Example.com’s hosts, and across these platforms the OpenSSH package has different names:
How are we going to ensure Puppet installs the correctly-named package for each platform? The answer lies with Facter, Puppet’s system inventory tool. During each Puppet run, Facter queries data about the host and sends it to the Puppet master. This data includes the operating system of the host, which is made available in our Puppet manifests as a variable called$operatingsystem
. We can now use this variable to select the appropriate package name for each platform. Let’s rewrite ourPackage["openssh"]
resource:
package { "ssh":
name => $operatingsystem ?
/(Red Hat|CentOS|Fedora|Ubuntu|Debian)/ => "openssh-server",
Solaris => "openssh",
},
ensure => installed,
}
You can see we’ve changed the title of our resource tossh
and specified a new attribute calledname
. As we explained in
Chapter 1
, each resource is made up of a type, title and a series of attributes. Each resource’s attributes includes its “name variable,” or ”namevar,” and the value of this attribute is used to determine the name of the resource. For example, the Package and Service resources use thename
attribute as their namevar while the File type uses thepath
attribute as its namevar. Most of the time we wouldn’t specify the namevar, as it is synonymous with the title, for example in this resource:
file { "/etc/passwd":
…
}
We don’t need to specify the namevar because the value will be taken from the title,"/etc/passwd"
. But often we’re referring to resources in many places and we might want a simple alias, so we can give the resource a title and specify its namevar this way:
file { "passwd":
path => "/etc/passwd",
…
}
We can now refer to this resource asFile["passwd"]
as an aliased short-hand.
Note
You should also read about thealias
metaparameter, which provides a similar capability, athttp://docs.puppetlabs.com/references/latest/metaparameter.html#alias
.
In our current example, the name of the package we’re managing varies on different hosts. Therefore, we want to specify a generic name for the resource and a platform-selected value for the actual package to be installed.
You can see that inside this newname
attribute we’ve specified the value of the attribute as$operatingsystem
followed by a conditional syntax that Puppet calls a “selector.” To construct a selector, we specify the a variable containing the value we want to select on as the value of our attribute, here$operatingsystem
, and follow this with a question mark (?). We then list on new lines a series of selections, for example if the value of$operatingsystem
is Solaris, then the value of thename
attribute will be set toopenssh,
and so on. Notice that we can specify multiple values in the form of simple regular expressions, like /(Solaris|Ubuntu|Debian)/
.
Note
Selector matching is case-insensitive. You can also see some other examples of regular expressions in selectors athttp://docs.puppetlabs.com/guides/language_tutorial.html#selectors
.