Create a simple class

Now that all the setup work is done, you can start writing some code. You are going to develop this module in a behavior/test driven manner, meaning you will write tests to describe how the module should behave before writing the actual code.

We will create an openldap::client class that, when declared, will install the OpenLDAP clients tools, including the ldapsearch command, so that we can connect to an LDAP server.

WRITE YOUR FIRST ACCEPTANCE TEST

GETTING STARTED

Now for the first tests, we will create a test to validate the openldap::client class' behavior in spec/acceptance/openldap__client_spec.rb. By convention, you should use a double underscore to replace the double colon in the class name.

The first thing to do in any test file is require our spec_helper_acceptance.rb file.

require 'spec_helper_acceptance'

describe 'openldap::client' do
  # tests will go here
end

THE FIRST ACCEPTANCE TEST: TESTING THAT THE PUPPET CATALOG CONVERGES AT FIRST RUN

require 'spec_helper_acceptance'

describe 'openldap::client' do
  describe 'running puppet code' do
    it 'should work with no errors' do
      pp = <<-EOS
        class { 'openldap::client': }
      EOS

      # Run it twice and test for idempotency
      apply_manifest(pp, :catch_failures => true)
      apply_manifest(pp, :catch_changes => true)
    end
  end
end

LINE 6 – 8

Store the Puppet manifest to run with apply_manifest() in the pp variable.

LINE 10

Run the manifest and catch any failure.

LINE 11

Run the manifest a second time and catch any change. Your manifest should converge at first run.

If you run your tests right now, you should see a nasty block of errors because the openldap::client class doesn’t actually exist yet. Let’s test with the centos7 nodeset:

$ BEAKER_set=centos-7 bundle exec rspec spec/acceptance/openldap__client_spec.rb
Beaker::Hypervisor, found some docker boxes to create
Provisioning docker
provisioning centos-7-x64
Using docker server at 0.0.0.0
...
openldap::client
  running puppet code
localhost $ scp /tmp/beaker20151109-12989-iond0f centos-7-x64:/tmp/apply_manifest.pp.Biy2Qi {:ignore => }
    should work with no errors (FAILED - 1)
Warning: ssh connection to centos-7-x64 has been terminated
Cleaning up docker

Failures:

  1) openldap::client running puppet code should work with no errors
     Failure/Error: apply_manifest(pp, :catch_failures => true)
     Beaker::Host::CommandFailure:
       Host 'centos-7-x64' exited with 1 running:
        puppet apply --verbose --detailed-exitcodes /tmp/apply_manifest.pp.Biy2Qi
       Last 10 lines of output were:
           Info: Loading facts
           Error: Evaluation Error: Error while evaluating a Resource Statement, Could not find declared class openldap::client at /tmp/apply_manifest.pp.Biy2Qi:1:9 on node centos-7-x64.wrk.cby.camptocamp.com
...
Finished in 12.43 seconds (files took 4 minutes 10.8 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/acceptance/openldap__client_spec.rb:5 # openldap::client running puppet code should work with no errors

LINE 1

The command to launch includes the nodeset to use in the environment variable BEAKER_set and specify the acceptance test to run.

LINE 6 – 7

The label of the describe blocks in the acceptance test (line 3 and 4 of spec/acceptance/openldap__client_spec.rb)

LINE 8

Scp the Puppet manifests to run to th container

LINE 9

Result of the test (failure)

LINE 14 – 23

Detail of the failure

LINE 25

Acceptance test run timer summary

LINE 25

Acceptance test result summary

LINE 27 – 29

Failure summary It basically says that the Puppet catalog does not even compile because it can’t find the openldap::client class. So let’s write a unit test that verifies that, at least, the catalog compiles.

WRITE YOUR FIRST UNIT TEST

GETTING STARTED

We’ll now write a unit test for our openldap::client class that validates that the Puppet catalog compiles. Unit tests for classes lives in spec/classes, so let’s create the directory first.

$ mkdir spec/classes

Then, we’ll create a unit test file for the class openldap::client in spec/classes/openldap__client_spec.rb.

The first thing to do in any test file is to require our spec_helper.rb file.

require 'spec_helper'

describe 'openldap::client' do
  # tests will go here
end

THE FIRST UNIT TEST : TESTING THAT THE CATALOG COMPILES

As first test, you should always be sure that your catalog compiles.

require 'spec_helper'

describe 'openldap::client' do
  it { is_expected.to compile.with_all_deps }
end

LINE 4

Test that verifies that the catalog actually compiles.

If you run your test right now, you should also see a big block of errors because the class does not exist yet.

$ bundle exec rake spec SPEC_OPTS=-fd
Notice: Preparing to install into /home/puppet-tdd/puppet-openldap/spec/fixtures/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/puppet-tdd/puppet-openldap/spec/fixtures/modules
└── puppetlabs-stdlib (v4.9.0)
...
openldap::client
  should compile into a catalogue without dependency cycles (FAILED - 1)

Failures:

  1) openldap::client should compile into a catalogue without dependency cycles
     Failure/Error: it { is_expected.to compile.with_all_deps }
       error during compilation: Evaluation Error: Error while evaluating a Function Call, Could not find class ::openldap::client for foo.example.com at line 1:1 on node foo.example.com
     # ./spec/classes/openldap__client_spec.rb:4:in `block (2 levels) in '

Finished in 0.14854 seconds (files took 0.6331 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/classes/openldap__client_spec.rb:4 # openldap::client should compile into a catalogue without dependency cycles

LINE 1

The command to run to launch unit tests. It tells bundler to exec the rake task spec (defined in puppetlabs_spec_helper) with the environment variable SPEC_OPTS set to -d which sets the output format to documentation for a cleaner output.

LINE 2 – 6

Install dependencies fixtures from .fixtures.yml into spec/fixtures

LINE 8

The label of the describe block (line 3 of spec/classes/openldap__client_spec.rb)

LINE 9

Result of the unit test

LINE 11 – 18

Details of the failures

LINE 19

Unit test timer summary

LINE 20

Unit test summary

LINE 21 – 23

Failures summary

WRITE THE CLASS

GETTING STARTED

Now that everything is set up, let’s write the actual Puppet code! First, create the directory where our manifests will live.

$ mkdir manifests

Next, create our openldap::client class into manifests/client.pp

class openldap::client {
}

If you save and run the unit tests again now, you’ll see that now that the class exists, our unit test passes.

$ bundle exec rake spec SPEC_OPTS=-fd
...
openldap::client
  should compile into a catalogue without dependency cycles

Finished in 0.15845 seconds (files took 0.62593 seconds to load)
1 example, 0 failures

and our acceptance test also passes.

$ BEAKER_set=centos-7 bundle exec rspec spec/acceptance/openldap__client_spec.rb
...
Beaker::Hypervisor, found some docker boxes to create
Provisioning docker
provisioning centos-7-x64
...
openldap::client
  running puppet code
localhost $ scp /tmp/beaker20151109-13967-1rfktlz centos-7-x64:/tmp/apply_manifest.pp.zjO9WQ {:ignore => }
localhost $ scp /tmp/beaker20151109-13967-cx62q5 centos-7-x64:/tmp/apply_manifest.pp.2P1ikI {:ignore => }
    should work with no errors
Warning: ssh connection to centos-7-x64 has been terminated
Cleaning up docker

Finished in 16.44 seconds (files took 3 minutes 15.3 seconds to load)
1 example, 0 failures

but it doesn’t yet do what we want… Let’s add the acceptance test that describe what we really want, i.e. be able to connect to an ldap server.

THE NEXT CHECK: TESTING THAT WE CAN ACTUALLY CONNECT TO AN LDAP SERVER

You’ll test that you can really connect to a public ldap test server with ldapsearch. First, you need to create a method that will launch the ldapsearch command. Put it in your spec_helper_acceptance.rb so that you can use it in all your acceptance test files

def ldapsearch(cmd, exit_codes = [0,1], &block)
  shell("ldapsearch #{cmd}", :acceptable_exit_codes => exit_codes, &block)
end

LINE 1

Function declaration

LINE 2

Use the shell beaker DSL function to actually launch command

Now you can use it in your acceptance test:

require 'spec_helper_acceptance'

describe 'openldap::client' do
  describe 'running puppet code' do
    it 'should work with no errors' do
      pp = <<-EOS
        class { 'openldap::client': }
      EOS

      # Run it twice and test for idempotency
      apply_manifest(pp, :catch_failures => true)
      apply_manifest(pp, :catch_changes => true)
    end

    it 'can connect to an ldap test server with ldapsearch' do
      ldapsearch('-LLL -h ldap.forumsys.com -D "uid=tesla,dc=example,dc=com" -b "dc=example,dc=com" -w password') do |r|
        expect(r.stdout).to match(/dn: dc=example,dc=com/)
      end
    end
  end
end

LINE 15 – 19

Declare a new test that actually runs the ldapsearch command. If you save and run beaker you’ll have an error because it doesn’t find the ldapsearch command:

BEAKER_set=centos-7 bundle exec rspec spec/acceptance/openldap__client_spec.rb
...
Beaker::Hypervisor, found some docker boxes to create
Provisioning docker
provisioning centos-7-x64
Using docker server at 0.0.0.0
...
openldap::client
  running puppet code
localhost $ scp /tmp/beaker20151109-14665-oorjrm centos-7-x64:/tmp/apply_manifest.pp.mTwar0 {:ignore => }
localhost $ scp /tmp/beaker20151109-14665-k4dxcy centos-7-x64:/tmp/apply_manifest.pp.WoIEM1 {:ignore => }
    should work with no errors
    can connect to an ldap test server with ldapsearch (FAILED - 1)
Warning: ssh connection to centos-7-x64 has been terminated
Cleaning up docker

Failures:

  1) openldap::client running puppet code can connect to an ldap test server with ldapsearch
     Failure/Error: ldapsearch('-LLL -h ldap.forumsys.com -D "uid=tesla,dc=example,dc=com" -b "dc=example,dc=com" -w password') do |r|
     Beaker::Host::CommandFailure:
       Host 'centos-7-x64' exited with 127 running:
        ldapsearch -LLL -h ldap.forumsys.com -D "uid=tesla,dc=example,dc=com" -b "dc=example,dc=com" -w password
       Last 10 lines of output were:
           bash: ldapsearch: command not found
...
Finished in 16.24 seconds (files took 3 minutes 35.2 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/acceptance/openldap__client_spec.rb:15 # openldap::client running puppet code can connect to an ldap test server with ldapsearch

The ldapsearch command is available in the openldap-client package on RedHat, so let’s be sure that a package resource with named openldap-clients exists in the catalog.

describe ‘openldap::clientdo
  it { is_expected.to compile.with_all_deps }
  it { is_expected.to contain_package('openldap-clients').with(
    {
      :ensure => :present,
    }
  ) }
end

If you save and run the unit tests, you should have an error because you don’t actually have a file resource named openldap-clients in your catalog

$ bundle exec rake spec SPEC_OPTS=-fd
...
openldap::client
  should compile the catalogue without cycles
  should contain Package[openldap-clients] with ensure => :present (FAILED - 1)

Failures:

  1) openldap::client should contain Package[openldap-clients] with ensure => :present
     Failure/Error: it { is_expected.to contain_package('openldap-clients').with(
       expected that the catalogue would contain Package[openldap-clients]
     # ./spec/classes/openldap__client_spec.rb:5:in `block (2 levels) in '
...
Finished in 0.79859 seconds (files took 0.72733 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/classes/openldap__client_spec.rb:5 # openldap::client should contain Package[openldap-clients] with ensure => :present

So, let’s fix this adding the installation of ldap client tools in openldap::client class:

class openldap::client {
  package { 'openldap-clients':
    ensure => present,
  }
}

And launch the unit tests again:

$ bundle exec rake spec SPEC_OPTS=-fd
...
openldap::client
  should compile the catalogue without cycles
  should contain Package[openldap-clients] with ensure => :present
...
Finished in 1.24 seconds (files took 0.69623 seconds to load)
2 examples, 0 failures

Then, the acceptance tests:

$ BEAKER_set=centos-7 bundle exec rspec spec/acceptance/openldap__client_spec.rb 
...
Beaker::Hypervisor, found some docker boxes to create
Provisioning docker
provisioning centos-7-x64
Using docker server at 0.0.0.0
...
openldap::client
  running puppet code
localhost $ scp /tmp/beaker20151109-15756-1jslev3 centos-7-x64:/tmp/apply_manifest.pp.leqXMR {:ignore => }
localhost $ scp /tmp/beaker20151109-15756-ipwpl3 centos-7-x64:/tmp/apply_manifest.pp.Qf66ii {:ignore => }
    should work with no errors
    can connect to an ldap test server with ldapsearch
Warning: ssh connection to centos-7-x64 has been terminated
Cleaning up docker

Finished in 18.18 seconds (files took 3 minutes 7 seconds to load)
2 examples, 0 failures

Now we have the behavior we wanted. We can connect to an ldap server. At least on Centos/RedHat7… But what happens if you run the tests on Debian 7? Let’s see…

$ BEAKER_set=debian-7 bundle exec rspec spec/acceptance/openldap__client_spec.rb 
...
Beaker::Hypervisor, found some docker boxes to create
Provisioning docker
provisioning debian-7-x64
...
openldap::client
  running puppet code
localhost $ scp /tmp/beaker20151109-17819-1cgpd9x debian-7-x64:/tmp/apply_manifest.pp.GSJRBb {:ignore => }
    should work with no errors (FAILED - 1)
    can connect to an ldap test server with ldapsearch (FAILED - 2)
Warning: ssh connection to debian-7-x64 has been terminated
Cleaning up docker

Failures:

  1) openldap::client running puppet code should work with no errors
     Failure/Error: apply_manifest(pp, :catch_failures => true)
     Beaker::Host::CommandFailure:
       Host 'debian-7-x64' exited with 4 running:
        puppet apply --verbose --detailed-exitcodes /tmp/apply_manifest.pp.GSJRBb
       Last 10 lines of output were:
           Error: Execution of '/usr/bin/apt-get -q -y -o DPkg::Options::=--force-confold install openldap-clients' returned 100: Reading package lists...
           Building dependency tree...
           Reading state information...
           E: Unable to locate package openldap-clients
           Error: /Stage[main]/Openldap::Client/Package[openldap-clients]/ensure: change from purged to present failed: Execution of '/usr/bin/apt-get -q -y -o DPkg::Options::=--force-confold install openldap-clients' returned 100: Reading package lists...
           Building dependency tree...
           Reading state information...
           E: Unable to locate package openldap-clients
           Info: Creating state file /opt/puppetlabs/puppet/cache/state/state.yaml
           Notice: Applied catalog in 0.90 seconds
...
  2) openldap::client running puppet code can connect to an ldap test server with ldapsearch
     Failure/Error: ldapsearch('-LLL -h ldap.forumsys.com -D "uid=tesla,dc=example,dc=com" -b "dc=example,dc=com" -w password') do |r|
     Beaker::Host::CommandFailure:
       Host 'debian-7-x64' exited with 127 running:
        ldapsearch -LLL -h ldap.forumsys.com -D "uid=tesla,dc=example,dc=com" -b "dc=example,dc=com" -w password
       Last 10 lines of output were:
           bash: ldapsearch: command not found
...
Finished in 14.53 seconds (files took 2 minutes 32.3 seconds to load)
2 examples, 2 failures

Failed examples:

rspec ./spec/acceptance/openldap__client_spec.rb:5 # openldap::client running puppet code should work with no errors
rspec ./spec/acceptance/openldap__client_spec.rb:15 # openldap::client running puppet code can connect to an ldap test server with ldapsearch

It fails because the package openldap-clients does not exist on Debian and thus, the ldapsearch command is not installed.

On Debian, the ldapsearch command lives in the ldap-utils package and not in openldap-clients.

We have to be sure that, on Debian family OS, Puppet installs the ldap-utils package and not the openldap-clients package. For that, we can test the content of the catalog with a unit tests.

rspec-puppet sends the :facts hash to the Puppet compiler to populate the facts or top scope variables you use in your manifests.

We have to add some logic in our unit test because our catalog will be different.

You can either populate the :facts hash yourself:

require 'spec_helper'

describe 'openldap::client' do
  context 'on Debian7' do
    let(:facts) { { :osfamily => 'Debian' } }
    it { is_expected.to compile.with_all_deps }
    it { is_expected.to contain_package('openldap-clients').with(
      {
        :ensure => :present,
        :name   => 'ldap-utils',
      }
    ) }
  end
  context 'on RedHat' do
    let(:facts) { { :osfamily => 'RedHat' } }
    it { is_expected.to compile.with_all_deps }
    it { is_expected.to contain_package('openldap-clients').with(
      {
        :ensure => :present,
        :name   => 'openldap-clients',
      }
    ) }
  end
end

Or, probably better, let rspec-puppet-facts populate the :facts hash for you and avoid duplicate code:

require 'spec_helper'

describe 'openldap::client' do
  on_supported_os.each do |os, facts|
    context "on #{os}" do
      let(:facts) do
        facts
      end

      it { is_expected.to compile.with_all_deps }

      case facts[:osfamily]
      when 'Debian'
        it { is_expected.to contain_package('openldap-clients').with(
          {
            :ensure => :present,
            :name   => 'ldap-utils',
          }
        ) }
      else
        it { is_expected.to contain_package('openldap-clients').with(
          {
            :ensure => :present,
            :name   => 'openldap-clients',
          }
        ) }
      end
    end
  end
end

LINE 4

Loop over every supported operating system declared in your metadata.json.

Add the rspec-puppet-facts gem to your Gemfile:

source 'https://rubygems.org'

gem 'puppet',                 :require => false
gem 'beaker-rspec',           :require => false
gem 'puppetlabs_spec_helper', :require => false
gem 'rspec-puppet-facts',     :require => false

And update your gemset:

$ bundle update

Load rspec-puppet-facts in your spec/spec_helper.rb

require 'puppetlabs_spec_helper/module_spec_helper'
require 'rspec-puppet-facts'
include RspecPuppetFacts

Then, run the unit tests again

$ bundle exec rake spec SPEC_OPTS=-fd
...
openldap::client
  on debian-7-x86_64
    should compile the catalogue without cycles
    should contain Package[openldap-clients] with ensure => :present and name => "ldap-utils" (FAILED - 1)
  on redhat-7-x86_64
    should compile the catalogue without cycles
    should contain Package[openldap-clients] with ensure => :present and name => "openldap-clients"

Failures:

  1) openldap::client on debian-7-x86_64 should contain Package[openldap-clients] with ensure => :present and name => "ldap-utils"
     Failure/Error: it { is_expected.to contain_package('openldap-clients').with(
       expected that the catalogue would contain Package[openldap-clients] with name set to "ldap-utils" but it is set to "openldap-clients"
     # ./spec/classes/openldap__client_spec.rb:15:in `block (4 levels) in <top (required)>'
...
Finished in 1.01 seconds (files took 0.69753 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/classes/openldap__client_spec.rb:15 # openldap::client on debian-7-x86_64 should contain Package[openldap-clients] with ensure => :present and name => "ldap-utils"

Now we can fix the openldap::client class:

class openldap::client {
  $package_name = $::osfamily ? {
    'Debian' => 'ldap-utils',
    'RedHat' => 'openldap-clients',
  }
  package { 'openldap-clients':
    ensure => present,
    name   => $package_name,
  }
}

And run the tests again

$ bundle exec rake spec SPEC_OPTS=-fd
...
openldap::client
  on debian-7-x86_64
    should compile the catalogue without cycles
    should contain Package[openldap-clients] with ensure => :present and name => "ldap-utils"
  on redhat-7-x86_64
    should compile the catalogue without cycles
    should contain Package[openldap-clients] with ensure => :present and name => "openldap-clients"
...
Finished in 1.1 seconds (files took 0.70695 seconds to load)
4 examples, 0 failures

It works!

Let’s try the acceptance test with the Debian7 nodeset to test the actual behavior:

$ BEAKER_set=debian-7 bundle exec rspec spec/acceptance/openldap__client_spec.rb 
...
openldap::client
  running puppet code
localhost $ scp /tmp/beaker20150301-11248-qtq1sw debian-7-x64:/tmp/apply_manifest.pp.zR7atA {:ignore => }
localhost $ scp /tmp/beaker20150301-11248-1uv28s2 debian-7-x64:/tmp/apply_manifest.pp.e6JdFV {:ignore => }
    should work with no errors
    can connect to an ldap test server with ldapsearch
Destroying vagrant boxes
==> debian-7-x64: Forcing shutdown of VM...
==> debian-7-x64: Destroying VM and associated drives...

Finished in 34.33 seconds (files took 4 minutes 8.8 seconds to load)
2 examples, 0 failures

It works too!

Now that you know how to develop a simple Puppet class in a behavior/test driver manner, we will see in Chapter 3 how to code a more complex class.