Published on 11 May 2011 by

Parameterized classes are a huge step forward in the Puppet language. They allow configurations to be composed as a collection of well-defined interfaces, expose those interfaces to introspection, and eliminate any order dependencies commonly encountered with dynamic scoping and include.

Below, I will share my experiences and thoughts about how to model configurations with parameterized classes.

The main difference between parameterized classes and include is the singleton nature of parameterized classes. Like other resources, they can only be declared once to prevent potential parameter conflicts.

Because of this, classes should generally avoid declaring other classes.

A declaration of a class in a single location precludes its declaration elsewhere, creating a tight coupling between the two classes which can result in class incompatibilities and a loss of encapsulation.

Example:

A class called tomcat may depend on a class called Java.

class tomcat {
  class { 'java': }
  ...
}

Another class called activemq may also depend on Java.

class activemq {
  class { 'java': }
 …
}

now these classes are incompatible:

class { ['activemq', 'tomcat']: }

Any attempt to install both tomcat and activemq on a node would result in the error message:

Duplicate definition: Class[Java] is already defined …


The best approach is to push these kinds of dependencies towards the Node.

Each class can specify that they have a dependency on the class without having to actually declare it:

 
class tomcat {
  Class['java'] -> Class['tomcat']
   …
}

class activemq {
  Class['java'] -> Class['activemq']
}

This allows for a reasonable error message in cases where the dependency is not satisfied. The consumer of a module gets an actionable error:

Could not find resource 'Class[Java]' for relationship on 'Class[Tomcat]'


Which alerts them to the fact that the java class should be declared somewhere in their code.

node mynode {
  class { ['java', 'activemq', 'tomcat']: }
}

The ability for the consumer of these modules to specify their own Java class becomes even more attractive in the case where the class is configurable.

node mynode {
  class { 'java':
    java_home => '/home/dan/java_home/'
  }
  class { ['activemq', 'tomcat']: }
}

Class Composition Exceptions

As we saw above, a class should generally not compose its dependencies because it causes tight coupling.

There are some cases where tight coupling between classes is exactly what we want.

In cases where tight coupling is appropriate, I like to think about it as the delegation of a class as being the single point of authority over another class. Any other components that need the functionality of the declared class would now have to go through the delegated class.

Example 1

Consider the pattern in Puppet where a module is composed of classes representing the package, configuration, and service.

This allows for greater re-usability of modules by allowing the consumer to select which parts of the configuration they want to perform.

In this pattern it makes sense to also have a single class that represents the default behavior of installing the package, configuring and starting the service.

class ssh::client(
  $permitrootlogin = 'true'
) {
  Class['ssh::package'] -> Class['ssh::config']
  Class['ssh::config'] ~> Class['ssh::service']
  class { 'ssh::package': }
  class { 'ssh::config': permitrootlogin => $permitrootlogin }
  class { 'ssh:service': }
}

In this case, other parts of our Puppet configuration are prohibited from being able to directly declare the individual package, config, or service classes. Any attempt to do so would result in an error which is exactly the behavior that we want.

class { 'ssh::client': permitrootlogin => true}
class { 'ssh::config': permitrootlogin => false } 
Duplicate definition: Class[Ssh::Config]


Example 2

It is also acceptable to compose classes in order to create classes used for assignment of roles to nodes.

Nodes can often be thought of as being classified by two main aspects, its platform (or base OS configuration) and by its role.

Since in general nodes can be thought about as having a single base OS configuration as well as role (where a role could be composed of multiple roles), these high level classes should be management as a composition of other classes without creating conflicts.

Known Issues

There are a few issues with the approaches mentioned above:

  1. The above syntax
    Class['java'] -> Class['tomcat']
    



    not only marks the class as being a dependency, but also specifies order dependencies. There is no way to mark a class as being required without effecting ordering.

  2. If this pattern becomes pervasive, it would be nice to have a keyword (like Self) that can be used to make relationships to the containing class.

    (#5824)


    This becomes particularly useful for defined resources:

    define apache::vhost() {
      Class['apache']->Apache::Vhost[$name]
    }
    
  3. Puppet disallows redeclaration of parameterized classes, even in cases where there are no conflicting parameters or even when there are no parameters at all. This makes it more difficult for classes to satisfy their own dependencies.


    (#7487)

Share via:
Posted in:

Add new comment

The content of this field is kept private and will not be shown publicly.

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.