Creating custom types
So you want to create a custom type. Good for you. There should be more of them in the world. However, it can be really daunting to create a type, as the existing types are mostly very complex, with talk of providers, munging, and all the rest of it. Funnily enough, most of the types we might want to create are a hell of a lot simpler than the pre-existing ones, and don't need the majority of the complexity that Puppet's type model provides.
This document is a lighter introduction to writing types than the more official docs. It simplifies the model down to the bare minimum needed to implement a wide range of simple (but still useful) types. Hopefully you can get up to speed with writing simple types, and then move on to more difficult concepts later once you need them.
Building a mental model
It's important to know roughly how Puppet does what it does with types before writing one, so we'll start with a bit of explanation about how Puppet works with types.
There's two important things that Puppet asks each resource during a Puppet run: "''Are you up to date?''" and "''Please modify the system to bring yourself up to date''". The work that gets done by your type can be many and varied, but it all must fit into this question / action model.
For an example of this, consider the simple task of appending a line to a file if it doesn't already exist. The question "are you up to date?" is handled by just looking through the file to see if the line exists. Modifying the system just involves appending the line to the file.
If you can describe what you want to do with your type in these terms, you're a long way towards being able to write your type.
Simple Beginnings: Defining the type
There's a bunch of stuff you have to do to define any type in the Puppet system -- scaffolding, if you like. A simple template looks like this:
module Puppet newtype(:append_if_no_such_line) do end end
Here we're defining a type named append_if_no_such_line, which isn't going to do anything. We'll fill in the blanks later.
Where should you be putting this code? It's important that the file be named after your type: a type named append_if_no_such_line must be put in a file named append_if_no_such_line.rb. If you put it in any other file, Puppet won't recognise it and your type won't get used (probably resulting in errors in your manifest).
What about the directory to put the file into? Puppet will look for types in a puppet/type directory anywhere in the Ruby load path and in the libdir you've defined. (If you don't know where your libdir setting points to, you can run puppetd --genconfig to get all your config variables and see what it's set to).
Since you don't want to be copying a type definition file to every machine under Puppet control, you can use pluginsync to get the copying done automatically. Setting that up is beyond the scope of this article; see CreatingCustomTypes for info on the pluginsync mechanism.
Variables: how to know what to do
It would be a very rare type indeed that didn't need to be configured in some way. Puppet's way of defining variables that can be set in the manifest is with "''parameters''". You define these in your type, and then you can specify values for them in your manifests.
Continuing with our append_if_no_such_line example, we have two parameters of interest -- the file we want to mangle, and the line we want to append. So let's define those now:
module Puppet
newtype(:append_if_no_such_line) do
newparam(:file)
newparam(:line)
end
end
Defining parameters is pretty easy, huh? We've missed one thing in our type above, a parameter that every type needs but which we don't think about very often. Any thoughts? It's the parameter which defines the "title" of each individual resource, often called the "name". In theory, you can call this anything you like, but there's complications if you call it anything other than "`name`", so we'll just call it that and move on:
module Puppet
newtype(:append_if_no_such_line) do
newparam(:name)
newparam(:file)
newparam(:line)
end
end
Documentation is Critical
I'm going to take a little pause here and talk about documenting your type. It's important to document your type so others can use it. Unfortunately, Puppet uses it's own slightly weird structure for documentation, by setting variables in various places, rather than just using rdoc comments. Oh well.
To document the type itself, you need to set the @doc variable at the type level with a documentation string:
module Puppet
newtype(:append_if_no_such_line) do
@doc = "Ensure that the given line is defined in the file, and append
the line to the end of the file if the line isn't already in the file."
newparam(:name)
newparam(:file)
newparam(:line)
end
end
Documenting parameters (and other things we'll look at later) is done by calling desc in a block attached to the parameter:
module Puppet
newtype(:append_if_no_such_line) do
@doc = "Ensure that the given line is defined in the file, and append
the line to the end of the file if the line isn't already in the file."
newparam(:name) do
desc "The name of the resource"
end
newparam(:file) do
desc "The file to examine (and possibly modify) for the line."
end
newparam(:line) do
desc "The line we're interested in."
end
end
end
You can use restructured text in the docstring if you like. Yep, another text formatting language. Huzzah!
Properties: Doing Things For Fun and Profit
The concept of "doing things" in Puppet is abstracted out into a concept called a "''property''". I always get properties and parameters confused -- the only way I've found to remember which is which is to write "Properties Do Things; Parameters Are Variables" on a piece of paper stuck on the wall behind my monitor. I suggest you do the same.
For maximum confusion, properties are also parameters, in the sense that you can assign values to them in your manifest. We're not going to stuff around with all the possibilities here, but just use a very simple model to get the job done. We're going to use one property, called "{{{ensure}}}", and put all our code onto that property to do what we need done.
First, we define the property:
module Puppet
newtype(:append_if_no_such_line) do
...
newproperty(:ensure) do
desc "Whether the resource is in sync or not."
end
end
end
Note the nice documentation we've already added, just like we did for parameters.
Schizophrenia means never being alone
Properties have a fairly confusing facet to their existence, in that they effectively have two values: what the the property ''should'' be, and what the property actually ''is''. The purpose of the property is to try and get the ''should'' and ''is'' to match, by changing the system. When we're talking about the "value" of a property, sometimes we're talking about the ''should'' value and sometimes we're talking about the ''is'' value. I'll try and make sure and indicate which one whenever it might be unclear.
"Am I up to date?"
Every property needs a way to retrieve it's "current value" on the system (which is the ''is'' value). Surprisingly, this is pretty easy to do, by defining the retrieve method inside the property. All this method should do is take a peek at the system and return a "current value". Note that the retrieve method should never make any changes to the system, because it gets called regularly.
For the append_if_no_such_line type, our retrieve method is pretty simple:
module Puppet
newtype(:append_if_no_such_line) do
...
newproperty(:ensure) do
desc "Whether the resource is in sync or not."
def retrieve
File.readlines(resource[:file]).map { |l|
l.chomp
}.include?(resource[:line]) ? :insync : :outofsync
end
end
end
end
This retrieve is about as simple as you can get: if the file includes the interesting line, return the symbol :insync, otherwise return the symbol :outofsync.
One point to note in this method is the calls to resource[:file] and resource[:line]. This is simply the way you retrieve the value of parameters in property code.
What Puppet is doing internally is to compare what the is value provided by retrieve with the should value of the property. How is the should value set? You can set it in your manifest (does "ensure => present" ring a bell?) but we're just going to set the should value as a default:
module Puppet
newtype(:append_if_no_such_line) do
...
newproperty(:ensure) do
desc "Whether the resource is in sync or not."
defaultto :insync
def retrieve
File.readlines(resource[:file]).map { |l|
l.chomp
}.include?(resource[:line]) ? :insync : :outofsync
end
end
end
end
This is pretty reasonable, because -- let's face it -- when are we not going to want the property to be in sync?
So the question "are we in sync?" is actually answered by comparing the ''is'' value provided by retrieve with the should value set in the manifest or with defaultto. If the values match, then this resource doesn't need to be worked on, and Puppet moves on to some other resource. Otherwise, it's time to actually do something.
Property Values in this area are skyrocketing
Time for another sidebar. Puppet has a bunch of ways to define the possible values for a property, but we're going to stick with defining a set of valid values and sticking with that. This is really simple:
module Puppet
newtype(:append_if_no_such_line) do
...
newproperty(:ensure) do
...
newvalue :outofsync
newvalue :insync
end
end
end
There's more stuff you can do with values, which we'll look at next.
Making Changes
If Puppet decides it needs to make a change, it needs some code to run. There's a bunch of possible mechanisms that Puppet can use, but in our simple type, we're going to go with what I'm calling the "value method". Earlier we saw how to define a possible value for a property, but I didn't show any of the good bits. What you can do is give a call to newvalue a block of code, and if Puppet decides that it wants the property to have that value, then it'll call the block of code. In other words, what you're saying is "the code in this block will make this property of this resource equal to this value". Example? Certainly, monsieur:
module Puppet
newtype(:append_if_no_such_line) do
...
newproperty(:ensure) do
...
newvalue :outofsync
newvalue :insync do
File.open(resource[:file], 'a') { |fd| fd.puts resource[:line] }
end
end
end
end
Again, we're calling resource[:file] and resource[:line] to get the values of the important parameters in the resource.
Putting it all together
Here is the complete type, ready for deployment:
module Puppet
newtype(:append_if_no_such_line) do
@doc = "Ensure that the given line is defined in the file, and append
the line to the end of the file if the line isn't already in
the file."
newparam(:name) do
desc "The name of the resource"
end
newparam(:file) do
desc "The file to examine (and possibly modify) for the line."
end
newparam(:line) do
desc "The line we're interested in."
end
newproperty(:ensure) do
desc "Whether the resource is in sync or not."
defaultto :insync
def retrieve
File.readlines(resource[:file]).map { |l|
l.chomp
}.include?(resource[:line]) ? :insync : :outofsync
end
newvalue :outofsync
newvalue :insync do
File.open(resource[:file], 'a') { |fd| fd.puts resource[:line] }
end
end
end
end
Fin!
Hopefully you've got enough info to write your own simple types. One word of advice: if you're developing a type and you find yourself thinking "this is getting damned complex", then the chances are that some of Puppet's more advanced features might help you -- so it's time to look at pages like CreatingCustomTypes, CompleteResourceExample, and ProviderDevelopment. Hit the puppet developer list for discussion on more complex issues relating to type development.