This article is a part of this blog post series:
- Puppet types and providers development part 1: creating the type
- Puppet types and providers development part 2: creating a resource
- Puppet types and providers development part 3: on resource uniqueness
- Puppet types and providers development part 4: caching resource properties to improve performance
- Puppet types and providers development part 5: self.instances
- Puppet types and providers development part 6: mysteries of self.prefetch
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
- It needs to work in a gazillion of environments and operating systems
- 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:
- Fun With Puppet Providers - Part 1 of Whatever
- Who Abstracted My Ruby?
- 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.