Authors: Jeffrey McCune James Turnbull
You can see that we’ve tried to define the$package
variable twice. If we were to try to compile and apply this configuration, the Puppet agent would return the following error:
err: Cannot reassign variable package at /etc/puppet/modules/ssh/manifests/init.pp:5
Note
The error helpfully also tells us the file, and line number in the file, where we’ve tried to redefine the variable.
So what’s a scope? Each class, definition, or node introduces a new scope, and there is also a top scope for everything defined outside of those structures. Scopes are created hierarchically and the important thing you need to remember about scope hierarchy is that it is created when Puppet code is evaluated, rather than when it is defined, for example:
$package = "openssh"
class ssh {
package { $package:
ensure => installed,
}
}
class ssh_server
include ssh
$package = "openssh-server"
}
include ssh_server
Here a top level scope, in which$package
is defined, is present. Then there’s a scope for thessh_server
class and a scope below that for thessh
class. When Puppet runs the$package
variable will have a value of"openssh-server"
because this is what the variable was when evaluation occurred.
Naturally, in these different scopes, you can reassign the value of a variable:
class apache {
$apache = 1
}
class passenger {
$apache = 2
}
The same variable can be used and defined in both theapache
andpassenger
classes without generating an error because they represent different scopes.
Going back to node inheritance, you can probably see how this dynamic scoping is going to be potentially confusing, for example:
class apache {
$apacheversion = "2.0.33"
package { "apache2":
ensure => $apacheversion,
}
}
node 'web.example.com' {
include apache
}
node 'web2.example.com' inherits 'web.example.com' {
$apacheversion = "2.0.42"
}
Here we’ve created a class calledapache
and a package resource for theapache2
package. We’ve also created a variable called$apacheversion
and used that as the value of theensure
attribute of the package resource. This tells Puppet that we want to install version 2.0.33 of Apache. We’ve then included ourapache
class in a node,web.example.com
.
But we’ve also decided to create another node,web2.example.com
, which inherits the contents of theweb.example.com
node. In this case, however, we’ve decided to install a different Apache version and therefore we specified a new value for the$apacheversion
variable. But instead of using this new value, Puppet will continue to install the 2.0.33 version of Apache because the$apacheversion
variable is maintained in its original scope of theweb.example.com
node and the new variable value is ignored.
There is a work-around for this issue that you can see here:
class apache {
$apacheversion = "2.0.33"
package { "apache2":
ensure => $apacheversion,
}
}
class base {
include apache
}
node 'web.example.com' {
$apacheversion = "2.0.42"
include base
}
Instead of defining abase
node we’ve defined abase
class that includes theapache
class. When we created our node, we specified the$apacheversion
we want and then included thebase
class, ensuring we’re in the right scope. We could put other like items in ourbase
class and specify any required variables.
Note
You can learn more about variable scoping, workarounds and related issues athttp://projects.puppetlabs.com/projects/puppet/wiki/Frequently_Asked_Questions#Common+Misconceptions
.
With Puppet installed and node definitions in place, we can now move on to creating our modules for the various hosts. But first, let’s do a quick refresher on modules in general.
In
Chapter 1
, we learned about modules: self-contained collections of resources, classes, files that can be served, and templates for configuration files. We’re going to use several modules to define the various facets of each host’s configuration. For example, we will have a module for managing Apache on our Web server and another for managing Postfix on our mail server.
Recall that modules are structured collections of Puppet manifests. By default Puppet will search the module path, which is by default/etc/puppet/modules/
and/var/lib/puppet/modules
, for modules and load them. These paths are controlled by themodulepath
configuration option. This means we don’t need to import any of these files into Puppet – it all happens automatically.
It’s very important that modules are structured properly. For example, oursudo
module contains the following:
sudo/
sudo/manifests
sudo/manifests/init.pp
sudo/files
sudo/templates
Inside ourinit.pp
we create a class with the name of our module:
class sudo {
configuration…
}
Lastly, we also discovered we can apply a module, like thesudo
module we created in
Chapter 1
, to a node by using theinclude
function like so:
node 'puppet.example.com' {
include sudo
}
The included function adds the resources contained in a class or module, for example adding all the resources contained in thesudo
module here to the nodepuppet.example.com
.
Let’s now see how to manage the contents of our modules using version control tools as we recommended in
Chapter 1
.
Note
You don’t have to always create your own modules. The Puppet Forge athttp://forge.puppetlabs.com
contains a large collection of pre-existing modules that you can either use immediately or modify to suit your environment. This can make getting started with Puppet extremely simple and fast.
Because modules present self-contained collections of configuration, we also want to appropriately manage the contents of these modules, allowing us to perform change control. To manage your content, we recommend that you use a Version Control System or VCS.
Version control is the method most developers use to track changes in their application source code. Version control records the state of a series of files or objects and allows you to periodically capture that state in the form of a revision. This allows you to track the history of changes in files and objects and potentially revert to an earlier revision should you make a mistake. This makes management of our configuration much easier and saves us from issues like undoing inappropriate changes or accidently deleting configuration data.
In this case, we’re going to show you an example of managing your Puppet manifests with a tool called Git, which is a distributed version control system (DVCS). Distributed version control allows the tracking of changes across multiple hosts, making it easier to allow multiple people to work on our modules. Git is used by a lot of large development projects, such as the Linux kernel, and was originally developed by Linux Torvalds for that purpose. It’s a powerful tool but it’sd easy to learn the basic steps. You can obviously easily use whatever version control system suits your environment, for example many people use Subversion or CVS for the same purpose.
First, we need to install Git. On most platforms we install thegit
package. For example, on Red Hat and Ubuntu:
$ sudo yum install git
or,
$ sudo apt-get install git
Once Git is installed, let’s identify ourselves to Git so it can track who we are and associate some details with actions we take.
$ git config --global user.name "Your Name"
$ git config --global user.email [email protected]
Now let’s version control the path containing our modules, in our case/etc/puppet/modules
. We change to that directory and then execute thegit
binary to initialize our new Git repository.
$ cd /etc/puppet/modules
$ git init
This creates a directory called.git
in the/etc/puppet/modules
directory that will hold all the details and tracking data for our Git repository.
We can now add files to this repository using thegit
binary with theadd
option.
$ git add *
This adds everything currently in our path to Git. You can also usegit
and therm
option to remove items you don’t want to be in the repository.
$ git rm filename
This doesn’t mean, however, that our modules are already fully tracked by our Git repository. Like Subversion and other version control systems, we need to “commit” the objects we’d like to track. The commit process captures the state of the objects we’d like to track and manage, and it creates a revision to mark that state. You can also create a file called.gitignore
in the directory. Every file or directory specified in this file will be ignored by Git and never added.
Before we commit though, we can see what Git is about by using thegit status
command:
$ git status
This tells us that when we commit that Git will add the contents to the repository and create a revision based on that state.
Now let’s commit our revision to the repository.
$ git commit –a –m "This is our initial commit"
The–m
option specifies a commit message that allows us to document the revision we’re about to commit. It’s useful to be verbose here and explain what you have changed and why, so it’s easier to find out what’s in each revision and make it easier to find an appropriate point to return to if required. If you need more space for your commit message you can omit the–m
option and Git will open your default editor and allow you to type a more comprehensive message.
The changes are now committed to the repository and we can use thegit log
command to see our recent commit.
$ git log
We can see some information here about our commit. First, Git uses SHA1 hashes to track revisions; Subversion, for example, uses numeric numbers – 1, 2, 3, etc. Each commit has a unique hash assigned to it. We will also see some details about who created the commit and our commit message telling us what the commit is all about.