Type development

When you define a resource type, focus on what the resource can do, not how it does it.

Note: Unless you are maintaining existing type and provider code, or the Resource API limitations affect you, use the Resource API to create custom resource types, instead of this method.

Creating types

Types are created by calling the newtype method on the Puppet::Type class:
# lib/puppet/type/database.rb
Puppet::Type.newtype(:database) do
  @doc = "Create a new database."
  # ... the code ...
end
The name of the type is the only required argument to newtype. The name must be a Ruby symbol, and the name of the file containing the type must match the type's name.

The newtype method also requires a block of code, specified with either curly braces ({ ... }) or the do ... end syntax. The code block implements the type, and contains all of the properties and parameters. The block will not be passed any arguments.

You can optionally specify a self-refresh option for the type by putting :self_refresh => true after the name. Doing so causes resources of this type to refresh (as if they had received an event through a notify-subscribe relationship) whenever a change is made to the resource. A notable use of this option is in the core mount type.

Documenting types

Write a description for the custom resource type in the type's @doc instance variable. The description can be extracted by the puppet doc --reference type command, which generates a complete type reference which includes your new type, and by the puppet describe command, which outputs information about specific types.

Write the description as a string in standard Markdown format. When the Puppet tools extract the string, they strip the greatest common amount of leading whitespace from the front of each line, excluding the first line. For example:
Puppet::Type.newtype(:database) do
  @doc = %q{Creates a new database. Depending
    on the provider, this might create relational
    databases or NoSQL document stores.

    Example:

        database {'mydatabase':
          ensure => present,
          owner  => root,
        }
  }
end
In this example, any whitespace would be trimmed from the first line (in this case, it’s zero spaces), then the greatest common amount would be trimmed from remaining lines. Three lines have four leading spaces, two lines have six, and two lines have eight, so four leading spaces would be trimmed from each line. This leaves the example code block indented by four spaces, and thus doesn’t break the Markdown formatting.

Properties and parameters

The bulk of a type definition consists of properties and parameters, which become the resource attributes available when declaring a resource of the new type.

The difference between a property and a parameter is subtle but important:
  • Properties correspond to something measurable on the target system. For example, the UID and GID of a user account are properties, because their current state can be queried or changed. In practical terms, setting a value for a property causes a method to be called on the provider.
  • Parameters change how Puppet manages a resource, but do not necessarily map directly to something measurable. For example, the user type’s managehome attribute is a parameter — its value affects what Puppet does, but the question of whether Puppet is managing a home directory isn’t an innate property of the user account.

Additionally, there are a few special attributes called metaparameters, which are supported by all resource types. These don’t need to be handled when creating new types; they’re implemented elsewhere.

A type definition typically has multiple properties, and must have at least one parameter.

Properties

A custom type's properties are at the heart of defining how the resource works. In most cases, it’s the properties that interact with your resource’s providers.

If you define a property named owner, then when you are retrieving the state of your resource, then the owner property calls the owner method on the provider. In turn, when you are setting the state (because the resource is out of sync), then the owner property calls the owner= method to set the state on disk.

There’s one common exception to this: The ensure property is special because it’s used to create and destroy resources. You can set this property up on your resource type just by calling the ensurable method in your type definition:
Puppet::Type.newtype(:database) do
  ensurable
  ...
end
This property uses three methods on the provider: create, destroy, and exists?. The last method, somewhat obviously, is a Boolean to determine if the resource exists. If a resource’s ensure property is out of sync, then no other properties are checked or modified.

You can modify how ensure behaves, such as by adding other valid values and determining what methods get called as a result; see types like package for examples.

The rest of the properties are defined a lot like you define the types, with the newproperty method, which should be called on the type:
Puppet::Type.newtype(:database) do
  ensurable
  newproperty(:owner) do
    desc "The owner of the database."
    ...
  end
end
Note the call to desc; this sets the documentation string for this property, and for Puppet types that get distributed with Puppet, it is extracted as part of the Type reference.
When Puppet was first developed, there would typically be a lot of code in this property definition. Now, however, you only define valid values or set up validation and munging. If you specify valid values, then Puppet only accepts those values, and automatically handles accepting either strings or symbols. In most cases, you only define allowed values for ensure, but it works for other properties, too:
newproperty(:enable) do
  newvalue(:true)
  newvalue(:false)
end
You can attach code to the value definitions (this code would be called instead of the property= method), but it’s normally unnecessary.
For most properties, though, it is sufficient to set up validation:
newproperty(:owner) do
  validate do |value|
    unless value =~ /^\w+/
      raise ArgumentError, "%s is not a valid user name" % value
    end
  end
end
Note that the order in which you define your properties can be important: Puppet keeps track of the definition order, and it always checks and fixes properties in the order they are defined.

Customizing behavior

By default, if a property is assigned multiple values in an array:
  • It is considered in sync if any of those values matches the current value.

  • If none of those values match, the first one is used when syncing the property.

If, instead, the property should only be in sync if all values match the current value (for example, a list of times in a cron job), declare this:
newproperty(:minute, :array_matching => :all) do # :array_matching defaults to :first
  ...
end

You can also customize how information about your property gets logged. You can create an is_to_s method to change how the current values are described, should_to_s to change how the desired values are logged, and change_to_s to change the overall log message for changes. See current types for examples.

Handling property values

When a resource is created with a list of desired values, those values are stored in each property in its @should instance variable. You can retrieve those values directly by calling should on your resource (although note that when :array_matching is set to :first you get the first value in the array; otherwise you get the whole array):
myval = should(:color)
When you’re not sure (or don’t care) whether you’re dealing with a property or parameter, it’s best to use value:
myvalue = value(:color)

Parameters

Parameters are defined the same way as properties. The difference between them is that parameters never result in methods being called on providers.

To define a new parameter, call the newparam method. This method takes the name of the parameter (as a symbol) as its argument, as well as a block of code. You can and should provide documentation for each parameter by calling the desc method inside its block. Tools that generate docs from this description trim leading whitespace from multiline strings, as described for type descriptions.
newparam(:name) do
  desc "The name of the database."
end

Namevar

Every type must have at least one mandatory parameter: the namevar. This parameter uniquely identifies each resource of the type on the target system — for example, the path of a file on disk, the name of a user account, or the name of a package.

If the user doesn’t specify a value for the namevar when declaring a resource, its value defaults to the title of the resource.

There are three ways to designate a namevar. Every type must have exactly one parameter that meets exactly one of these criteria:
  1. Create a parameter whose name is :name. Because most types just use :name as the namevar, it gets special treatment and automatically becomes the namevar.
    newparam(:name) do
      desc "The name of the database."
    end
  2. Provide the :namevar => true option as an additional argument to the newparam call. This allows you to use a namevar with a different, more descriptive name, such as the file type’s path parameter.
    newparam(:path, :namevar => true) do
      ...
    end
  3. Call the isnamevar method (which takes no arguments) inside the parameter’s code block. This allows you to use a namevar with a different, more descriptive name. There is no practical difference between this and option 2.
    newparam(:path) do
      isnamevar
      ...
    end

Specifying allowed values

If your parameter has a fixed list of valid values, you can declare them all at the same time:
newparam(:color) do
  newvalues(:red, :green, :blue, :purple)
end
You can specify regular expressions in addition to literal values; matches against regex always happen after equality comparisons against literal values, and those matches are not converted to symbols. For instance, given the following definition:
newparam(:color) do
  desc "Your color, and stuff."

  newvalues(:blue, :red, /.+/)
end
If you provide blue as the value, then your parameter is set to :blue, but if you provide green, then it is set to "green".

Validation and munging

If your parameter does not have a defined list of values, or you need to convert the values in some way, you can use the validate and munge hooks:
newparam(:color) do
  desc "Your color, and stuff."

  newvalues(:blue, :red, /.+/)

  validate do |value|
    if value == "green"
      raise ArgumentError,
        "Everyone knows green databases don't have enough RAM"
    else
      super(value)
    end
  end

  munge do |value|
    case value
    when :mauve, :violet # are these colors really any different?
      :purple
    else
      super(value)
    end
  end
end
The default validate method looks for values defined using newvalues and if there are any values defined it accepts only those values (this is how allowed values are validated). The default munge method converts any values that are specifically allowed into symbols. If you override either of these methods, note that you lose this value handling and symbol conversion, which you’ll have to call super for.

Values are always validated before they’re munged.

Lastly, validation and munging only happen when a value is assigned. They have no role to play at all during use of a given value, only during assignment.

Boolean parameters

Boolean parameters are common. To avoid repetition, some utilities are available:
require 'puppet/parameter/boolean'
# ...
newparam(:force, :boolean => true, :parent => Puppet::Parameter::Boolean)
There are two parts here. The :parent => Puppet::Parameter::Boolean part configures the parameter to accept lots of names for true and false, to make things easy for your users. The :boolean => true creates a boolean method on the type class to return the value of the parameter. In this example, the method would be named force?.

Automatic relationships

Use automatic relationships to define the ordering of resources.

By default, Puppet includes and processes resources in the order they are defined in their manifest. However, there are times when resources need to be applied in a different order. The Puppet language provides ways to express explicit ordering such as relationship metaparameters (require, before, etc), chaining arrows and the require and contain functions.

Sometimes there is natural relationship between your custom type and other resource types. For example, ssh authorized keys can only be managed after you create the home directory and you can only manage files after you create their parent directories. You can add explicit relationships for these, but doing so can be restrictive for others who may want to use your custom type. Automatic relationships provide a way to define implicit ordering. For example, to automatically add a require relationship from your custom type to a configuration file that it depends on, add the following to your custom type:
autorequire(:file) do
  ['/path/to/file']
end
The Ruby symbol :file refers to the type of resource you want to require, and the array contains resource title(s) with which to create the require relationship(s). The effect is nearly equivalent to using an explicit require relationship:
custom { <CUSTOM RESOURCE>:
  ensure => present,
  require => File['/path/to/file']
}

An important difference between automatic and explicit relationships is that automatic relationships do not require the other resources to exist, while explicit relationships do.

Agent-side pre-run resource validation

A resource can have prerequisites on the target, without which it cannot be synced. In some cases, if the absence of these prerequisites would be catastrophic, you might want to halt the catalog run if you detect a missing prerequisite.

In this situation, define a method in your type named pre_run_check. This method can do any check you want. It should take no arguments, and should raise a Puppet::Error if the catalog run should be halted.

If a type has a pre_run_check method, Puppet agent and puppet apply runs the check for every resource of the type before attempting to apply the catalog. It collects any errors raised, and presents all of them before halting the catalog run.

As a trivial example, here’s a pre-run check that fails randomly, about one time out of six:
Puppet::Type.newtype(:thing) do
  newparam :name, :namevar => true

  def pre_run_check
    if(rand(6) == 0)
      raise Puppet::Error, "Puppet roulette failed, no catalog for you!"
    end
  end
end

How types and providers interact

The type definition declares the features that a provider must have and what’s required to make them work. Providers can either be tested for whether they suffice, or they can declare that they have the features. Because a type's properties call getter and setter methods on the providers, the providers must define getters and setters for each property (except ensure).

Additionally, individual properties and parameters in the type can declare that they require one or more specific features, and Puppet throws an error if those parameters are used with providers missing those features:
newtype(:coloring) do
  feature :paint, "The ability to paint.", :methods => [:paint]
  feature :draw, "The ability to draw."

  newparam(:color, :required_features => %w{paint}) do
    ...
  end
end
The first argument to the feature method is the name of the feature, the second argument is its description, and after that is a hash of options that help Puppet determine whether the feature is available. The only option currently supported is specifying one or more methods that must be defined on the provider. If no methods are specified, then the provider needs to specifically declare that it has that feature:
Puppet::Type.type(:coloring).provide(:drawer) do
  has_feature :draw
end
The provider can specify multiple available features at the same time with has_features.
When you define features on your type, Puppet automatically defines the following class methods on the provider:
  • feature?: Passed a feature name, returns true if the feature is available or false otherwise.
  • features: Returns a list of all supported features on the provider.
  • satisfies?: Passed a list of feature, returns true if they are all available, false otherwise.

Additionally, each feature gets a separate Boolean method, so the above example would result in a paint? method on the provider.