Structured design patterns

Your knowledge of classes and defined types is still rather academic. You have learned about their defining aspects and the syntax to use them, but we have yet to give you a feeling of how these concepts come to bear in different real-life scenarios.

The following sections will present an overview of what you can do with these language tools.

Writing comprehensive classes

Many classes are written to make Puppet perform momentous tasks on the agent platform. Of these, the Apache class is probably one of the more modest examples. You can conceive a class that can be included from any machine's manifest and make sure that the following conditions are met:

  • The firewalling software is installed and configured with a default ruleset
  • The malware detection software is installed
  • Cron jobs run the scanners in set intervals
  • The mailing subsystem is configured to make sure the cron jobs can deliver their output

There are two general ways you can go about the task of creating a class of this magnitude. It can either become what one might call a monolithic implementation, a class with a large body that comprises all resources that work together to form the desired security baseline. On the other hand, you could aim for a composite design, with few resources (or none at all) in the class body, and a number of include statements for simpler classes instead. The functionality is compartmentalized, and the central class acts as a collector.

We have not yet touched on the ability of classes to include other classes. That's because it's quite simple. The body of a class can comprise almost any manifest, and the include statement is no exception. Among the few things that cannot appear in a class are node blocks.

Adding some life to the descriptions, this is how the respective classes will roughly look like:

class monolithic_security { 
  package { [ 'iptables', 'rkhunter', 'postfix' ]:
    ensure => 'installed';
  } 
  cron { 'run-rkhunter': 
    ... 
  } 
  file { '/etc/init.d/iptables-firewall': 
    source => ... 
    mode   => 755 
  }
  file { '/etc/postfix/main.cf': 
    ensure  => 'file', 
    content => ... 
  } 
  service { [ 'postfix', 'iptables-firewall' ]: 
    ensure => 'running', 
    enable => true 
  } 
}

class divided_security {
  include iptables_firewall
  include rkhunter
  include postfix
}

When developing your own functional classes, you should not try to pick either of these extremes. Most classes will end up anywhere on the spectrum in between. The choice can be largely based on your personal preference. The technical implications are subtle, but these are the respective drawbacks:

  • Consequently aiming for monolithic classes opens you up to resource clashes, because you take almost no advantage of the singleton nature of classes
  • Splitting up classes too much can make it difficult to impose order and distribute refresh events—you can refer to the Establishing relationships among containers section later in this chapter

Neither of these aspects is of critical importance at most times. The case-by-case design choices will be based on each author's experience and preference. When in doubt, lean towards composite designs at first.

Writing component classes

There is another common use case for classes. Instead of filling a class with lots of aspects that work together to achieve a complex goal, you can also limit the class to a very specific purpose. Some classes will contain but one resource. The class wraps the resource, so to speak.

This is useful for resources that are needed in different contexts. By wrapping them away in a class, you can make sure that those contexts do not create multiple declarations of the same resource.

For example, the netcat package can be useful to firewall servers, but also to web application servers. There is probably a firewall class and an appserver class. Both declare the netcat package:

package { 'netcat': 
  ensure => 'installed' 
} 

If any server ever has both roles (this might happen for budget reasons or in other unforeseen circumstances), it is a problem; when both the firewall and appserver classes are included, the resulting manifest declares the netcat package twice. This is forbidden. To resolve this situation, the package resource can be wrapped in a netcat class, which is included by both the firewall and appserver classes:

class netcat {
  package { 'netcat': 
    ensure => 'installed' 
  } 
} 

Let's consider another typical example for component classes that ensures the presence of some common file path. Assume your IT policy requires all custom scripts and applications to be installed in /opt/company/bin. Many classes, such as firewall and appserver from the previous example, will need some relevant content there. Each class needs to make sure that the directories exist before a script can be deployed inside it. This will be implemented by including a component class that wraps the file resources of the directory tree:

class scripts_directory { 
  file { [ '/opt/company/', '/opt/company/bin' ]: 
    ensure => 'directory', 
    owner  => 'root', 
    group  => 'root', 
    mode   => '0644', 
  } 
}

The component class is a pretty precise concept. However, as you have seen in the previous section about the more powerful classes, the whole range of possible class designs forms a fine-grained scale between the presented examples. All manifests you write will likely comprise more than a few classes. The best way to get a feeling for the best practices is to just go ahead and use classes to build the manifests you need.

Note

The terms comprehensive class and component class are not official Puppet language, and the community does not use them to communicate design practices. We chose them arbitrarily to describe the ideas we laid out in these sections. The same holds true for the descriptions of the use cases for defined types, which will be seen in the next sections.

Next, let's look at some uses for defined types.

Using defined types as resource wrappers

For all their apparent similarity to classes, defined types are used in different ways. For example, the component class was described as wrapping a resource. This is accurate in a very specific context—the wrapped resource is a singleton, and it can only appear in one form throughout the manifest.

When wrapping a resource in a defined type instead, you end up with a variation on the respective resource type. The manifest can contain an arbitrary number of instances of the defined type, and each will wrap a distinct resource.

Tip

For this to work, the name of the resource that is declared in the body of the defined type must be dynamically created. It is almost always the $name variable of the respective defined type instance, or a value derived from it.

Here is yet another typical example from the many manifests out there: most users who make use of Puppet's file serving capabilities will want to wrap the file type at some point so that the respective URL need not be typed for each file:

define module_file(String $module) { 
  file { $name: 
    source => "puppet:///modules/${module}/${name}"
  }
}

This makes it easy to get Puppet to sync files from the master to the agent. The master copy must be properly placed in the named modules on the master:

module_file { '/etc/ntpd.conf': 
  module => 'ntp':
} 

This resource will make Puppet retrieve the ntp.conf file from the ntp module. The preceding declaration is more concise and less redundant than the fully written file resource with the Puppet URL (especially for the large number of files you might need to synchronize), which would resemble the following:

file { '/etc/ntpd.conf': 
  source => 'puppet:///modules/ntp/etc/ntpd.conf':
} 

For a wrapper such as module_file, which will likely be used very widely, you will want to make sure that it supports all attributes of the wrapped resource type. In this case, the module_file wrapper should accept all file attributes. For example, this is how you add the mode attribute to the wrapper type:

define module_file(
  String $module,
  Optional[String] $mode = undef
) { 
  if $mode != undef { 
    File { mode => $mode } 
  } 
  file { $name: 
    source => "puppet:///modules/${module}/${name}" 
  } 
}

The File { ... } block declares some default values for all file resource attributes in the same scope. The undef value is similar to Ruby's nil, and is a convenient parameter default value, because it is very unlikely that a user will need to pass it as an actual value for the wrapped resource.

Tip

You can employ the override syntax instead of the default syntax as well:

File[$name] { mode => $mode }

This makes the intent of the code slightly more obvious, but is not necessary in the presence of just one file resource. Chapter 6, Leveraging the Full Toolset of the Language, holds more information about overrides and defaults.

Using defined types as resource multiplexers

Wrapping single resources with a defined type is useful, but sometimes you will want to add functionality beyond the resource type you are wrapping. At other times, you might wish for your defined type to unify a lot of functionality, just like the comprehensive classes from the beginning of the section.

For both scenarios, what you want to have is multiple resources in the body of your defined type. There is a classic example for this as well:

define user_with_key(
  String $key, 
  Optional[String] $uid = undef, 
  String $group = 'users'
) {
  user { $title: 
    ensure     => present
    gid        => $group, 
    managehome => true, 
  } -> 
  ssh_authorized_key { "key for ${title}": 
    ensure => present, 
    user   => $title, 
    type   => 'rsa', 
    key    => $key, 
  } 
}

This code allows you to create user accounts with authorized SSH keys in one resource declaration. This code sample has some notable aspects:

  • Since you are essentially wrapping multiple resource types, the titles of all inner resources are derived from the instance title (or name) of the current defined type instance; actually, this is a required practice for all defined types
  • You can hardcode parts of your business logic; in this example, we dispensed with the support for non-RSA SSH keys and defined users as the default group
  • Resources inside defined types can and should manage ordering among themselves (using the chaining arrow -> in this case)

Using defined types as macros

Some source code requires many repetitive tasks. Assume that your site uses a subsystem that relies on symbolic links at a certain location to enable configuration files, just like init does with the symlinks in rc2.d/ and its siblings, which point back to ../init.d/<service>.

A manifest that enables a large number of configuration snippets might look like this:

file { '/etc/example_app/conf.d.enabled/england': 
  ensure => 'link', 
  target => '../conf.d.available/england' 
}
file { '/etc/example_app/conf.d.enabled/ireland': 
  ensure => 'link', 
  target => '../conf.d.available/ireland' 
}
file { '/etc/example_app/conf.d.enabled/germany': 
  ensure => 'link', 
  target => '../conf.d.available/germany' 
  ... 
}

This is tiring to read and somewhat painful to maintain. In a C program, one would use a preprocessor macro that just takes the base name of both link and target and expands to the three lines of each resource description. Puppet does not use a preprocessor, but you can use defined types to achieve a similar result:

define example_app_config { 
  file { "/etc/example_app/conf.d.enabled/${name}": 
    ensure => 'link', 
    target => "../conf.d.available/${name}", 
  }
}

Tip

The defined type actually acts more like a simple function call than an actual macro.

The define requires no arguments—it can rely solely on its resource name, so the preceding code can now be simplified to the following:

example_app_config {'england': }
example_app_config {'ireland': }
example_app_config {'germany': }
...

Alternatively, the following code is even more terse:

example_app_config { [ 'england', 'ireland', 'germany', ... ]: 
}

This array notation leads us to another use of defined types.

Exploiting array values using defined types

One of the more common scenarios in programming is the requirement to accept an array value from some source and perform some task on each value. Puppet manifests are not exempt from this.

Let's assume that the symbolic links from the previous example actually led to directories, and that each such directory would contain a subdirectory to hold optional links to regions. Puppet should manage those links as well.

Of course, after learning about the macro aspect of defined types, you would not want to add each of those regions as distinct resources to your manifest. However, you will need to devise a way to map region names to countries. Seeing as there is already a defined resource type for countries, there is a very direct approach to this: make the list of regions an attribute (or rather, a parameter) of the defined type:

define example_app_config (
  Array $regions = []
) {
  file { "/etc/example_app/conf.d.enabled/${name}":
    ensure => link,
    target => "../conf.d.available/${name}",
  }
  # to do: add functionality for $regions
}

Using the parameter is straightforward:

example_app_config { 'england':
  regions => [ 'South East', 'London' ],
}
example_app_config { 'ireland':
  regions => [ 'Connacht', 'Ulster' ],
}
example_app_config { 'germany':
  regions => [ 'Berlin', 'Bayern', 'Hamburg' ],
}
...

The actual challenge is putting these values to use. A naïve approach is to add the following to the definition of example_app_config:

file { $regions:
  path   => "/etc/example_app/conf.d.enabled/${title}/ regions/${name}",
  ensure => 'link',
  target => "../../regions.available/${name}";
}

However, this will not work. The $name variable does not refer to the title of the file resource that is being declared. It actually refers, just like $title, to the name of the enclosing class or defined type (in this case, the country name). Still, the actual construct will seem quite familiar to you. The only missing piece here is yet another defined type:

define example_app_region(String $country) { 
  file { "/etc/example_app/conf.d.enabled/${country}/regions/${name}":
    ensure => 'link', 
    target => "../../regions.available/${name}",
  } 
} 

The complete definition of the example_app_config defined type should look like this then:

define example_app_config(Array $regions = []) {
  file { "/etc/example_app/conf.d.enabled/${name}": 
    ensure => 'link', 
    target => "../conf.d.available/${name}", 
  } 
  example_app_region { $regions: 
    country => $name,
  } 
}

The outer defined type adapts the behavior of the example_app_region type to its respective needs by passing its own resource name as a parameter value.

Using iterator functions

With Puppet 4 and later versions, you probably would not write code like the one in the previous section. Thanks to new language features, using defined types as iterators is no longer necessary. We will outline the alternative using the following examples, with a more thorough exploration in Chapter 7, New Features from Puppet 4.

The plain country links can now be declared from an Array using the each function:

[ 'england', 'ireland', 'germany' ].each |$country| { 
  file { "/etc/example_app/conf.d.enabled/${country}": 
    ensure => 'link', 
    target => "../conf.d.available/${country}",
  } 
} 

The regions can be declared from structured data. A hash suffices for this use case:

$region_data = { 
  'england' => [ 'South East', 'London' ], 
  'ireland' => [ 'Connacht', 'Ulster' ], 
  'germany' => [ 'Berlin', 'Bayern', 'Hamburg' ], 
}
$region_data.each |$country, $region_array| {
  $region_array.each |$region| {
    file { "/etc/example_app/conf.d.enabled/${country}/ regions/${region}":
        ensure => link,
        target => "../../regions.available/${region}",
    }
  }
}

In new manifests, you should prefer iteration using the each and map functions over using defined types for this purpose. You will find examples of the former in older manifest code, however. See Chapter 7, New Features from Puppet 4, for more information on the topic.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset