Creating Puppet types and providers is easy…

February 1st, 2010 by kartar Leave a reply »

types are used to manage individual configuration items.  has a package , a service , a user , etc.  Each has providers. Each handles the management of that configuration on a different platform or tool, for example the package has aptitude, yum, RPM, and DMG providers (amongst 22 others – what is wrong with people that they need to invent new packaging systems… but I digress).

There are a lot of types, in fact I think covers a pretty good spectrum of configuration items that need to be managed.  I don’t know of anything in particular that is missing that I can’t live without.  But there are little gaps that are annoying, I’d like network and firewall types for example, but creating both these types in a generic enough way to support multiple platforms would be, IMHO, a non-trivial problem. 

Another gap is VCS/DVCS management. A lot of people use source code in repositories to do things with (including install stuff from you bad people – package things … it’s healthier). currently relies on creating and removing these repositories with the exec (which executes scripts or binaries), for example:

exec { "svn co http://core.svn.wordpress.org/trunk/ /var/www/wp":
    creates => "/var/www/wp",
}

This is a bit ugly and it’d be a lot easier to write a to manage repositories. But types and providers are written in Ruby and really, really complex and hard to develop. Right? Right?

No. No, they are not… and I’m going to create a simple and to show you. :)

Here’s a very (very!) simple , called , for managing repositories. I’ve created providers for and as examples also. The first part of the is the itself – these are usually stored in lib// or distributed via modules (see the PluginsInModules page in the wiki). I’ll create a file called .rb.

$ touch repo.rb

And then populate the file:

Puppet::Type.newtype(:repo) do
    @doc = "Manage repos"
 
    ensurable
 
    newparam(:source) do
        desc "The repo source"
 
        validate do |value|
            if value =~ /^git/
                resource[:provider] = :git
            else
                resource[:provider] = :svn
            end
        end
 
        isnamevar
 
    end
 
    newparam(:path) do
        desc "Destination path"
 
        validate do |value|
            unless value =~ /^\/[a-z0-9]+/
                raise ArgumentError , "%s is not a valid file path" % value
            end
        end
    end
end

So – pretty simple. We create a block ::.newtype(:) do that creates a new , which we’ve called .

Inside the block we’ve got a @doc string. This is the documentation for the . Add whatever level of detail and examples in here that is required.

We’ve also got the ensurable statement. Ensurable provides some “automagic” that creates a basic ensure property. types use the ensure property to determine the state of a configuration item.

service { "sshd":
    ensure => present,
}

The ensurable statement tells to expect three methods: create, destroy and exists? in our . These methods, allow, respectively:

  • A command to create the resource
  • A command to delete the resource, and
  • A command to check for the existence of the resource

All we then need to do is specify these methods and their contents and creates the supporting infrastructure around them but more on this when we look at our providers.

Next, we’ve defined a new parameter – this one called source.

    newparam(:source) do
        desc "The repo source"
 
        validate do |value|
            if value =~ /^git/
                resource[:provider] = :git
            else
                resource[:provider] = :svn
            end
        end
 
        isnamevar
    end

The source parameter will tell the where to go to retrieve/clone/checkout our source .

In this parameter we’re also using a hook called validate. Normally used to check the value for appropriateness here we’re using it to take a guess at what to use. Our code says, if the source parameter starts with then use the , if not default to the Subversion . This is obviously fairly crude as a default and we can override this by defining the attribute in our resources:

provider => git,

We’ve also used another piece of automagic, isnamevar, to make this parameter the “name” variable for this . In -speak, the value of this parameter is used as the name of the resource.

(Types have two kinds of values – properties and parameters. Properties “do things”. They tell us HOW the works. We’ve only defined one property, ensure, by using the ensurable statement. Parameters are more like variables, they contain information relevant to configuring the resource the manages rather than “doing things”.)

Finally, we’ve defined another parameter, path.

    newparam(:path) do
        desc "Destination path"
 
        validate do |value|
            unless value =~ /^\/[a-z0-9]+/
                raise ArgumentError , "%s is not a valid file path" % value
            end
        end
    end

This is a variable value that specifies where the should put the cloned/checked-out . In this parameter we’ve again used the validate hook to create a block that checks the value for appropriateness. Here we’re just checking, very crudely, to make sure it looks like the destination path is a valid fully-qualified file path. We could also use this validation for the source parameter to confirm a valid source URL/location was being provided.

(You can also use another hook called munge to adjust the value of the parameter rather than validating it before passing it to the .)

And that is it for the .

Next, we need to create a for our . Let’s start with a Subversion like so:

require 'fileutils'
 
Puppet::Type.type(:repo).provide(:svn) do
    desc "SVN Support"
 
    commands :svncmd => "svn"
    commands :svnadmin => "svnadmin"
 
    def create
        svncmd "checkout", resource[:name], resource[:path]
    end
 
    def destroy
        FileUtils.rm_rf resource[:path]
    end
 
    def exists?
        File.directory? resource[:path]
    end
end

Up front we’ve required the fileutils library, which we’re going to use a method from. Next, we’ve defined the as a block:

Puppet::Type.type(:repo).provide(:svn) do

We tell that this is a called for the called .

Then we use a desc method that allows us to add some documentation to our .

Next, we define the commands that this will use, here the and svnadmin binaries, to manipulate our resource’s configuration.

    commands :svncmd => "svn"
    commands :svnadmin => "svnadmin"

uses these commands to determine if the is appropriate to use on a client, if can’t find these commands in the local path then it will disable the .

Next, we’ve defined three methods – create, destroy and exists?. Sounds familiar? Yep, these are the methods that the ensurable statement expects to find in the :

The create method ensures our resource is created. It uses the command to create a with a source of resource[:name] (remember the source parameter in our is also the name variable of the – we could also specify resource[:source] here too) and a destination of resource[:path] (the value of the path attribute).

The delete method ensures the deletion of the resource. In this case, it deletes the directory and files specified in the resource[:path] parameter.

Lastly, the exists? method checks to see if the resource exists. Its operation is pretty simple and closely linked with the value of the ensure attribute in the resource:

  • If exists? is false and ensure is present, then create method will be called.
  • If exists? is true and ensure is set to absent, then the destroy method will be called.

In this case the exists? method checks if there is already a directory at the location specified in the resource[:path] parameter.

So, let’s put all this together and create a resource with our new . I’ve assumed you’ve already distributed your and providers to . We can then create a resource like:

repo { "wp":
    source => "http://core.svn.wordpress.org/trunk/",
    path => "/var/www/wp",
    ensure => present,
}

Simple eh? We specify a resource, the source we wish to check out or clone from, the destination path and the ensure attribute (present or absent) and that’s it.

You can see the complete code for this and its providers at my Puppet repository on GitHub. It’s obviously very basic but should be easy to extend to provide additional capabilities (and currently has no tests – my bad). You can find further documentation (in a lot more detail!) on creating your own types and providers at the Puppet wiki.

14 comments

  1. Dyresen says:

    A great tutorial :)

  2. Mike Pountney says:

    You pipped me to the post James – I have tried to make a start on this (a git/repo provider) over this weekend!

    I haven’t got to the actual module code yet, but have worked out how to include rspec tests into a puppet module – take a look at http://github.com/mikepea/puppet-git

    It’s a little heavy handed at the moment, with a lot of files copied from puppet itself, but they do run at least. I’ll take a fork of your repo monitor and write some tests for you, if you are happy with that.

    • kartar says:

      Interesting – I hadn’t thought about the implications of tests inside a module – that’s really interesting – you should open a feature request on that and maybe we can work to make it easier to do.

      And please feel free to hack on the type – it’s highly simplistic currently (and ugly in parts!) :)

  3. Marc says:

    Thanks for this excellent article ! Probably the most straightforward tutorial currently available on creating a puppet type. Would you consider making a page in puppet’s wiki from it ?

    The idea of checking out the content of a repository with puppet isn’t new: http://projects.reductivelabs.com/issues/184 and was mentioned again a few months ago: http://thread.gmane.org/gmane.comp.sysutils.puppet.devel/7432/focus=7462

    Be it using the file resource or a dedicated “repo” type, it would be nice to see something alike in one of puppet’s future releases !

    • kartar says:

      Hi Marc!

      Yeah I’ve been talking about fixing all those pages on the wiki – it’s on the never-ending TODO list. :)

      The idea isn’t new I know but I thought it would be a good example of a type/provider prototype for people. I believe there is some movement towards having a much more elegant “repo” type in a future Puppet release.

  4. Gary says:

    Very Nice,
    I had, naively, assumed that such a provider for git existed in Puppet. I was trying to write a completely automated recipe for all our Puppetmasters and was Googling for a recipe using a git provider. I’m very stoked to see what comes out of this article in the future. Great work guys!

  5. Mykel says:

    Hey James,
    Great post! It’s exactly the start of what I’m looking for.

    You mention that this code is available in your GitHub repo, but I can’t seem to locate it. Could you point me in the right direction?

  6. Andi says:

    great tutorial, helped me a lot since the original puppet docs are missing some important hints. thanks!

Leave a Reply