Puppet types and providers development part 1: creating the type

March 3, 2020 – Samuli Seppänen

This article is a part of this blog post series:

Writing Puppet types and providers looks like black magic even to those who have lots of experience in the Puppet language itself. While the official types development documentation is quite ok, provider documentation is definitely lacking. Not only are there no practical examples from real life, the important features such as prefetching, resource methods and flushing are described only on a very high level. The usual answer to lack of examples is "have a look at the built-in resources". But when you look at any built-in resource, you realize that the code is very complex because

  1. It needs to work in a gazillion of environments and operating systems
  2. It carries lots of legacy baggage (e.g. deprecated ways to do things)

What you're left with is the old but still useful book "Puppet Types and Providers" and Gary Larizza's very good blog series about the topic:

  1. Fun With Puppet Providers - Part 1 of Whatever
  2. Who Abstracted My Ruby?
  3. Seriously, What Is This Provider Doing?

But even Gary can't answer all the questions you might have. The chances are that your type/provider will be different enough from what the above sources assume that you'll run into trouble. For example you may be configuring resources through a remote API, and can't (or rather, don't want to) use self.prefetch (see above links) because you want to retain the possibility to have several API endpoints. But you still want the performance benefits using self.prefetch would bring with it. After all, you don't want your provider to be as slow as Ansible, do you?

Anyways, this blog post is about writing the Puppet type and providers, so let's go on with it. I will use my own librenms_service type (see puppet-librenms) as an example - it is basically a Puppet wrapper around the LibreNMS Services API.

I suggest always starting with the type - the model for the resource being managed - and making sure it works. There is really no need to make the provider work initially, but its skeleton has to be there. The type for librenms_service in lib/puppet/type/librenms_service.rb looks like this:


# frozen_string_literal: true

# LibreNMS service type for Puppet
module Puppet
  Type.newtype(:librenms_service) do
    require 'uri'

    @doc = 'Manage LibreNMS services'

   ensurable do
     desc 'Create or remove the device.'

     newvalue(:present) do
       provider.create
     end

     newvalue(:absent) do
       provider.destroy
     end

     defaultto :present
   end

    newparam(:hostname, namevar: true) do
      desc 'Hostname or IP of the device'
    end

    newparam(:url) do
      desc 'LibreNMS API endpoint (e.g. https://librenms.example.org/api/v0)'
      validate do |url|
        raise('Property auth_token must be a string') unless url.is_a?(String)
      end
    end

    newparam(:auth_token) do
      desc 'LibreNMS API token'
      validate do |auth_token|
        unless auth_token.is_a?(String)
          raise('Property auth_token must be a string')
        end
      end
    end

    newproperty(:type) do
      desc 'Type of the monitored service'
      validate do |type|
        unless type.is_a?(String)
          raise('Property type must be a string')
        end
      end
    end

    newproperty(:ip) do
      desc 'IP of the monitored service'
      validate do |ip|
        unless ip.is_a?(String)
          raise('Property IP must be a string')
        end
      end
    end

    newproperty(:desc) do
      desc 'Description'
      validate do |description|
        unless description.is_a?(String)
          raise('Property desc must be a string')
        end
      end
    end

    newproperty(:param) do
      desc 'Parameters for the service'
      validate do |param|
        unless param.is_a?(String)
          raise('Property param must be a string')
        end
      end
    end
  end
end

The ensurable block tells that we can create and remove LibreNMS services, instead of just managing the parameters of pre-existing services. The parameters are used to give Puppet the information it needs to locate (hostname) and manage (API key, API URL) a resource. Properties are the actual things you can manage in a LibreNMS service.

Once you have the type you're probably tempted to try it out with puppet apply with a manifest like this:

librenms_service { 'http-on-librenms.example.org':
   hostname   => 'librenms.example.org',
   url        => 'https://librenms.example.org/api/v0',
   auth_token => 'secret',
   type       => 'http',
   ip         => 'librenms.example.org',
   desc       => 'http',
   param      => 'C 50 --sni -S',
 }

But when you do you will get an obscure error seemingly related to the first parameter in the type

$ puppet apply --modulepath=modules librenms_service.pp 
Notice: Compiled catalog for laptop.example.org in environment production in 0.02 seconds  
Error: /Stage[main]/Main/Librenms_service[http-on-librenms.example.org]: Could not evaluate: undefined method `type' for nil:NilClass
Notice: Applied catalog in 0.02 seconds

When you comment out the first parameter to debug, Puppet will complain about the new first parameter. Non-obviously the reason is the lack of provider implementation - which can (and should) be a skeleton at this point. So we add a skeleton to lib/puppet/provider/librenms_service/api.rb:


# frozen_string_literal: true

Puppet::Type.type(:librenms_service).provide(:api) do
  require 'rubygems'
  require 'rest_client'
  require 'json'

  defaultfor kernel: 'Linux'

  def exists?
    true
  end

  def create
    true
  end

  def destroy
    true
  end

  def type
    'foobar'
  end

  def type=(value)
    'foobar'
  end

  def ip
    'foobar'
  end

  def ip=(value)
    'foobar'
  end

  def desc
    'foobar'
  end

  def desc=(value)
    'foobar'
  end

  def param
    'foobar'
  end

  def param=(value)
    'foobar'
  end
end

This is enough to keep puppet happy so that we can try the type out. It is required that the type can be loaded, i.e. that the module is copied/linked to the modules directory passed to puppet apply:

$ puppet apply --modulepath=modules librenms_service.pp 
Notice: Compiled catalog for laptop.example.org in environment production in 0.16 seconds
Notice: /Stage[main]/Main/Librenms_service[http-on-librenms.example.org]/type: type changed 'foobar' to 'http'
Notice: /Stage[main]/Main/Librenms_service[http-on-librenms.example.org]/ip: ip changed 'foobar' to 'librenms.example.org'
Notice: /Stage[main]/Main/Librenms_service[http-on-librenms.example.org]/desc: desc changed 'foobar' to 'http'
Notice: /Stage[main]/Main/Librenms_service[http-on-librenms.example.org]/param: param changed 'foobar' to 'C 50 --sni -S'
Notice: Applied catalog in 0.01 seconds

As the provider code is unimplemented Puppet will not be able to do anything, but it is able to try. The next step is to start creating the provider methods one by one.

...

Want to talk to an expert?

If you want to reach us, just send us a message or book a free call!
menucross-circle