Introducing classes and defined types
Puppet's equivalents to methods or functions are twofold: there are classes on one hand and defined types (also just referred to as defines) on the other.
They are similar at first glance, in that they both hold a chunk of reusable manifest code. There are big differences in the way each is used though. Let's take a look at classes first.
Defining and declaring classes
A Puppet class can be considered to be a container for resources. It is defined once and selected by all nodes that need to make use of the prepared functionality. Each class represents a well-known subset of a system's configuration.
For example, a classic use case is a class that installs the Apache web server and applies some basic settings. This class will look like the following:
class apache { package { 'apache2': ensure => 'installed', } file { '/etc/apache2/apache2.conf': ensure => 'file', source => 'puppet:///modules/apache/etc/apache2/apache2.conf', require => Package['apache2'], } service { 'apache2': enable => true, subscribe => File['/etc/apache2/apache2.conf', } }
All web server nodes will make use of this class. To this end, their manifests need to contain a simple statement:
include apache
This is referred to as including a class, or declaring it. If your apache
class is powerful enough to do all that is needed, this line might fully comprise a node
block's content:
node 'webserver01' { include apache }
Note
In your own setup, you will likely not write your own Apache class. You can use open source classes that are available through Puppet modules. Chapter 5, Extending Your Puppet Infrastructure with Modules, will give you all the details.
This already concludes our tour of classes in a nutshell. There is yet some more to discuss, of course, but let's take a look at defined types before that.
Creating and using defined types
A defined type can be imagined like a blueprint for a piece of manifest. Like a class, it mainly consists of a body of the manifest code. However, a defined type takes arguments and makes their values available in its body as local variables.
Here is another typical example of a defined type, the Apache virtual host configuration:
define virtual_host( String $content, String[3,3] $priority = '050' ) { file { "/etc/apache2/sites-available/${name}": ensure => 'file', owner => 'root', group => 'root', mode => '0644', content => $content } file { "/etc/apache2/sites-enabled/${priority}-${name}": ensure => 'link', target => "../sites-available/${name}"; } }
This code might still seem pretty cryptic. It will get clearer in the context of how it is actually used from other places in your manifest; the following code shows you how:
virtual_host { 'example.net': content => file('apache/vhosts/example.net') } virtual_host{ 'fallback': priority => '999', content => file('apache/vhosts/fallback') }
This is why the construct is called a defined type—you can now place what appear to be resources in your manifest, but you really call your own manifest code construct.
Note
When declaring multiple resources of the same type, like in the preceding code, you can do so in a single block and separate them with a semicolon:
virtual_host { 'example.net': content => 'foo'; 'fallback': priority => '999', content => ..., }
The official Style Guide forbids this syntax, but it can make manifests more readable and maintainable in some cases.
The virtual_host
type takes two arguments: the content
argument is mandatory and is used verbatim in the configuration file resource. Puppet will synchronize that file's content to what is specified in the manifest. The priority
argument is optional and its value becomes the file name prefix. If omitted, the respective virtual host definition uses the default priority of 050
.
Both parameters of this example type are of the String
type. For details about Puppet's variable type system, see Chapter 7, New Features from Puppet 4. It suffices to say that you can restrict parameters to certain value types. This is optional, however. You can omit the type name, and Puppet will accept any value for the parameter in question.
Also, each defined type can implicitly refer to the name (or title) by which it was called. In other words, each instance of your define gets a name, and you can access it through the $name
or $title
variable.
Note
There are a few other magic variables that are available in the body of a defined type. If a resource of the defined type is declared with a metaparameter such as require => …
, its value can be accessed through the $require
variable in the body. The variable value remains empty if the metaparameter is not used. This works for all metaparameters, such as before
, notify
, and all the others, but you will likely never need to make use of this. The metaparameters automatically do the right thing.
Understanding and leveraging the differences
The respective purposes of Puppet's class and defined type are very specific and they usually don't overlap.
The class declares resources and properties that are in some way centric to the system. A class is a finalized description of one or sometimes more aspects of your system as a whole. Whatever the class represents, it can only ever exist in one form; to Puppet, each class is implicitly a singleton, a fixed set of information that either applies to your system (the class is included), or not.
The typical resources you will encapsulate in a class for convenient inclusion in a manifest are as follows:
- One or more packages that should be installed (or removed)
- A specific configuration file in
/etc
- A common directory, needed to store scripts or configs for many subsystems
- Cron jobs that should be mostly identical on all applicable systems
The define is used for all things that exist in multiple instances. All aspects that appear in varying quantities in your system can possibly be modeled using this language construct. In this regard, the define is very similar to the full-fledged resource it mimics with its declaration syntax. Some of the typical contents of defined types are:
- Files in a
conf.d
style directory - Entries in an easily parseable file such as
/etc/hosts
- Apache virtual hosts
- Schemas in a database
- Rules in a firewall
The class's singleton nature is especially valuable because it prevents clashes in the form of multiple resource declarations. Remember that each resource must be unique to a catalog. For example, consider a second declaration of the Apache package:
package { 'apache2': }
This declaration can be anywhere in the manifest of one of your web servers (say, right in the node
block, next to include apache
); this additional declaration will prevent the successful compilation of a catalog.
Tip
The reason for the prevention of a successful compilation is that Puppet currently cannot make sure that both declarations represent the same target state, or can be merged to form a composite state. It is likely that multiple declarations of the same resource get in a conflict about the desired value of some property (for example, one declaration might want to ensure that a package is absent
, while the other needs it to be present
).
The virtue of the class is that there can be an arbitrary number of include
statements for the same class strewn throughout the manifest. Puppet will commit the class's contents to the catalog exactly once.
Note
Note that the uniqueness constraint for resources applies to defined types. No two instances of your own define can share the same name. Using a name twice or more produces a compiler error:
virtual_host { 'wordpress': content => file(...), priority => '011', } virtual_host { 'wordpress': content => '# Dummy vhost', priority => '600', }