Day 25 in the #vDM30in30

Image from https://flic.kr/p/6t59b

So, we’ve talked about the RAL, getters, setters and the resource command.

Now let’s talk about implementing a RAL interface of our own.

swap_file type and provider

I ended up implementing a RAL layer in my swap_file module, mainly as an exercise in figuring out how types and providers work.

It looks like this:

Puppet::Type.type(:swap_file).provide(:linux) do

  desc "Swap file management via `swapon`, `swapoff` and `mkswap`"

  confine  :kernel   => :linux
  commands :swapon   => 'swapon'
  commands :swapoff  => 'swapoff'
  commands :mkswap   => 'mkswap'

  mk_resource_methods

  def initialize(value={})
    super(value)
    @property_flush = {}
  end

  def self.get_swap_files
    swapfiles = swapon(['-s']).split("\n")
    swapfiles.shift
    swapfiles.sort
  end

  def self.prefetch(resources)
    instances.each do |prov|
      if resource = resources[prov.name]
        resource.provider = prov
      end
    end
  end

  def self.instances
    get_swap_files.collect do |swapfile_line|
      new(get_swapfile_properties(swapfile_line))
    end
  end

  def self.get_swapfile_properties(swapfile_line)
    swapfile_properties = {}

    # swapon -s output formats thus:
    # Filename        Type    Size  Used  Priority

    # Split on spaces
    output_array = swapfile_line.strip.split(/\s+/)

    # Assign properties based on headers
    swapfile_properties = {
      :ensure => :present,
      :name => output_array[0],
      :file => output_array[0],
      :type => output_array[1],
      :size => output_array[2],
      :used => output_array[3],
      :priority => output_array[4]
    }

    swapfile_properties[:provider] = :swap_file
    Puppet.debug "Swapfile: #{swapfile_properties.inspect}"
    swapfile_properties
  end

  def exists?
    @property_hash[:ensure] == :present
  end

  def create
    @property_flush[:ensure] = :present
  end

  def destroy
    @property_flush[:ensure] = :absent
  end

  def create_swap_file(file_path)
    mk_swap(file_path)
    swap_on(file_path)
  end

  def mk_swap(file_path)
    Puppet.debug "Running `mkswap #{file_path}`"
    output = mkswap([file_path])
    Puppet.debug "Returned value: #{output}`"
  end

  def swap_on(file_path)
    Puppet.debug "Running `swapon #{file_path}`"
    output = swapon([file_path])
    Puppet.debug "Returned value: #{output}"
  end

  def swap_off(file_path)
    Puppet.debug "Running `swapoff #{file_path}`"
    output = swapoff([file_path])
    Puppet.debug "Returned value: #{output}"
  end

  def set_swapfile
    if @property_flush[:ensure] == :absent
      swap_off(resource[:name])
      return
    end

    create_swap_file(resource[:name]) unless exists?
  end

  def flush
    set_swapfile
    # Collect the resources again once they've been changed (that way `puppet
    # resource` will show the correct values after changes have been made).
    @property_hash = self.class.get_swapfile_properties(resource[:name])
  end

end

Now, there’s a bunch of weird stuff in there, that to be honest even know I only vaguely understand…

I’d recommend reading Gary’s blog on the subject. He explains a lot of the idea behind things like the initialize and prefetch stuff.

Basically, don’t worry about it for now, lets stick to the bit we’ve talked about, the “setter”: self.instances

self.instances

So, just like the rpm provider, we need to implement a self.instances method.

So here we are:

def self.instances
  get_swap_files.collect do |swapfile_line|
    new(get_swapfile_properties(swapfile_line))
  end
end

To make the code easier to read, I’m breaking down the process into easier to manage methods:

  • Get current swap files as an output on the command line
  • Take those lines and turn them into a hash

get_swap_files

Getting those current swapfiles on a Linux machine is fairly simple: swapon -s.

def self.get_swap_files
  swapfiles = swapon(['-s']).split("\n")
  swapfiles.shift
  swapfiles.sort
end

We .shift the first result, which removes the element of the lines, which is the column lines.

swapon -s
Filename				Type		Size	Used	Priority
/swapfile                               file		262140	0	-1

So we’re left with just

/swapfile                               file		262140	0	-1

get_swapfile_properties

We then take those lines and break up the chunks and turn them into a valid swapfile resource (as we’ve defined the valid parameters in our type)

def self.get_swapfile_properties(swapfile_line)
    swapfile_properties = {}

    # swapon -s output formats thus:
    # Filename        Type    Size  Used  Priority

    # Split on spaces
    output_array = swapfile_line.strip.split(/\s+/)

    # Assign properties based on headers
    swapfile_properties = {
      :ensure => :present,
      :name => output_array[0],
      :file => output_array[0],
      :type => output_array[1],
      :size => output_array[2],
      :used => output_array[3],
      :priority => output_array[4]
    }

    swapfile_properties[:provider] = :swap_file
    Puppet.debug "Swapfile: #{swapfile_properties.inspect}"
    swapfile_properties
  end

So, let’s test that out.

Here’s the swapon -s command:

Filename				Type		Size	Used	Priority
/mnt/swap.1             file	     500732	   0	-1

And here’s our RAL command:

[root@homebox ~]# puppet resource swap_file
swap_file { '/mnt/swap.1':
  ensure   => 'present',
  priority => '-1',
  size     => '500732',
  type     => 'file',
  used     => '0',
}

And we can even test that with an rspec test:

require 'spec_helper'

describe Puppet::Type.type(:swap_file).provider(:linux) do

  let(:resource) { Puppet::Type.type(:swap_file).new(
    {
    :name     => '/tmp/swap',
    :size     => '1024',
    :provider => described_class.name
    }
  )}

  let(:provider) { resource.provider }

  let(:instance) { provider.class.instances.first }

  swapon_s_output = <<-EOS
Filename                        Type            Size    Used    Priority
/dev/sda2                       partition       4192956 0       -1
/dev/sda1                       partition       4454542 0       -2
  EOS

  swapon_line = <<-EOS
/dev/sda2                       partition       4192956 0       -1
  EOS

  mkswap_return = <<-EOS
Setting up swapspace version 1, size = 524284 KiB
no label, UUID=0e5e7c60-bbba-4089-a76c-2bb29c0f0839
  EOS

  swapon_line_to_hash = {
    :ensure => :present,
    :file => "/dev/sda2",
    :name => "/dev/sda2",
    :priority => "-1",
    :provider => :swap_file,
    :size => "4192956",
    :type => "partition",
    :used => "0",
  }

  before :each do
    Facter.stubs(:value).with(:kernel).returns('Linux')
    provider.class.stubs(:swapon).with(['-s']).returns(swapon_s_output)
  end

  describe 'self.instances' do
    it 'returns an array of swapfiles' do
      swapfiles      = provider.class.instances.collect {|x| x.name }
      swapfile_sizes = provider.class.instances.collect {|x| x.size }

      expect(swapfiles).to      include('/dev/sda1','/dev/sda2')
      expect(swapfile_sizes).to include('4192956','4454542')
    end
  end

So I’m mocking out the results of the various commands, and then I’m making sure that if I feed my provider that input, I get a collection of instances (provider.class.instances), which I then do some basic tests to see if it matches my mock.

Further reading

Ok, so that’s 4 blog posts about the RAL, hopefully you’ve got a better understanding of how all this stuff works.

It’s quite a deep concept in the Puppet world, and hopefully it gives you a bit of insight into how Puppet works under the hood.

The Docs

Developing types and providers

Further reading on the RAL