Puppet types and providers development part 2: creating a resource

March 7, 2020 

This blog post is a part of this blog post series:

In the previous blog post we created the Puppet type librenms_service and created a dummy skeleton for the provider implementation. We were able to use the type, but it did not yet do anything. In this blog post we move to implementing the create method, which in this case was the low-hanging fruit. At this point we do not need to worry about any existence checks (the exists? method) or trying to minimizing the number of API calls with self.prefetch or something similar.

The best first step is to usually do some experimentation outside of Puppet first. Some preliminary setup was required in this case on our disposable Vagrant librenms instance:

  • Install monitoring-plugins package (API failures were caused otherwise)
  • Install the rest-client gem with /opt/puppetlabs/puppet/bin/gem install rest-client
  • Create an API token in the LibreNMS WebUI

A simple test is in order to make sure services API calls work as expected. The simplest way to do that is to list services using the example curl command given in LibreNMS services API documentation:

$ curl -H 'X-Auth-Token: secret' http://12.168.152.10/api/v0/services
 {
     "status": "ok",
     "services": [
         []
     ],
     "count": 1

As we can see there are no services yet, so we add one, again with curl to ensure that we have a functional foundation:

$ curl -X POST -d '{"type":"http","ip": "librenms.vagrant.example.lan","desc":"test http","param": "C 50 --sni -S"}' -H 'X-Auth-Token: secret' http://192.168.152.10/api/v0/services/librenms.vagrant.example.lan
{
    "status": "ok",
    "message": "Service http has been added to device librenms.vagrant.example.lan (#3)"

As we can see, the API call was able to add the new service, so if our Ruby code fails it is a problem in the Ruby code, not in the API or LibreNMS itself.

Now we remove the service manually and implement both curl commands in plain Ruby. This way we can increase complexity gradually and ease testing, experimentation and debugging. First we ensure that Ruby is able to use the API to list the services - this is the trivial test:

#!/opt/puppetlabs/puppet/bin/ruby
#
# services.rb

require 'rubygems'
require 'rest_client'
require 'json'

url = 'http://192.168.152.10/api/v0'
token = 'secret'
hostname = 'librenms.vagrant.example.lan'

devices_response = RestClient.get "#{url}/services",
                                  accept: :json,
                                  content_type: :json,
                                  x_auth_token: token

body = JSON.parse(devices_response.body)
puts body

When we run this Ruby code it should output the same information as the first curl above:

$ chmod 755 services.rb
$ ./services.rb
{"status"=>"ok", "services"=>[[]], "count"=>1}

The next step is to create a new service in Ruby. So we add a POST call to the above file (services.rb):

data = { 'type'     => 'http',
          'ip'       => 'librenms.vagrant.example.lan',
          'desc'     => 'http',
          'param'    => 'C 50 --sni -S', }
 
devices_response = RestClient.post "#{url}/services/#{hostname}",
                                   data.to_json,
                                   x_auth_token: token

Now, when run the code services first get listed (as above) and then the new service gets added:

$ ./services.rb
{"status"=>"ok", "services"=>[[]], "count"=>1}
{"status"=>"ok", "services"=>[[{"service_id"=>4, "device_id"=>1, "service_ip"=>"librenms.vagrant.example.lan", "service_type"=>"http", "service_desc"=>"http", "service_param"=>"C 50 --sni -S", "service_ignore"=>0, "service_status"=>3, "service_changed"=>1583565842, "service_message"=>"Service not yet checked", "service_disabled"=>0, "service_ds"=>"{}"}]], "count"=>1}

Interestingly running the same API call again just creates another identical service instead of erroring out. But we don't have to worry about that as we will soon implement the exists? method to prevent duplicates.

The final step is to move all of this into the Puppet provider. As we don't have the exists? method ready yet we just make it return false so that Puppet will always try to create the service:

def exists?
  false
end

Then we migrate our Ruby code into the provider with minor improvements and modifications:

  def create
    data = { 'type'     => resource[:type],
             'ip'       => resource[:ip],
             'desc'     => resource[:desc],
             'param'    => resource[:param], }

    begin
      RestClient.post "#{resource[:url]}/services/#{resource[:name]}",
                      data.to_json,
                      x_auth_token: resource[:auth_token]
    rescue StandardError => e
      raise "LibreNMS add_service_to_host API call failed for resource #{resource[:name]}: #{e.message}."
    end
  end

In the create function the parameters get their values from the Puppet type and there is some error handling, but otherwise this is identical to our earlier Ruby test code.

After removing the service again we can use a simple Puppet manifest:

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

Now we can finally test our create method implementation:

$ puppet apply --modulepath modules services.pp
Notice: Compiled catalog for librenms.vagrant.example.lan in environment production in 0.19 seconds
Notice: /Stage[main]/Main/Librenms_service[http-on-librenms.vagrant.example.lan]/ensure: created
Notice: Applied catalog in 1.22 seconds

We can the use curl again to see what we just created:

$ curl -H 'X-Auth-Token: secret' http://192.168.152.10/api/v0/services
{
    "status": "ok",
    "services": [
        [
            {
                "service_id": 7,
                "device_id": 1,
                "service_ip": "librenms.vagrant.example.lan",
                "service_type": "http",
                "service_desc": "http",
                "service_param": "C 50 --sni -S",
                "service_ignore": 0,
                "service_status": 3,
                "service_changed": 1583566601,
                "service_message": "Service not yet checked",
                "service_disabled": 0,
                "service_ds": "{}"
            }
        ]
    ],
    "count": 1
}

Our create method is working as excepted.

Samuli Seppänen
Samuli Seppänen
Author archive
menucross-circle