Create a custom type and provider
Now that we can install our OpenLDAP server and ensure that it is running, we want to be able to manage OpenLDAP databases. For that, we will create an OpenLDAP Puppet type and a provider to manage databases using OpenLDAP’s live configuration API. We will do all this using TDD, of course!
WRITE THE ACCEPTANCE TESTS
First, let’s create an acceptance test in spec/acceptance/openldap_database_spec.rb
for our new type openldap_database
require 'spec_helper_acceptance'
describe 'openldap_database' do
context 'when running puppet code' do
it 'should apply idempotently' do
pp = <<-EOS
class { 'openldap::client': } ->
class { 'openldap::server': } ->
openldap_database { 'dc=foo,dc=com':
ensure => present,
}
EOS
# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
end
it 'can connect with ldapsearch' do
ldapsearch('-LLL -x -b "dc=foo,dc=com"') do |r|
expect(r.stdout).to match(/dn: dc=foo,dc=com/)
end
end
end
end
WRITE THE CUSTOM TYPE
Let’s write a custom Puppet type that has these properties:
- is ensurable
- has suffix as namevar
- has backend and directory properties
WRITE THE UNIT TESTS FOR THE OPENLDAP_DATABASE TYPE
Unit tests for Puppet types live in spec/unit/puppet/type
, so let’s create this directory first.
$ mkdir -p spec/unit/puppet/type
Then we can create our unit tests for our openldap_database
type in spec/unit/puppet/type/openldap_database_spec.rb
.
The first thing to do is to require our spec_helper.rb
file:
require 'spec_helper'
describe Puppet::Type.type(:openldap_database) do
# Tests will go here
end
First, let’s validate the attributes of our custom type. We have to be sure that it accepts suffix and provider parameters and ensure, backend and directory properties:
require 'spec_helper'
describe Puppet::Type.type(:openldap_database) do
on_supported_os.each do |os, facts|
context "on #{os}" do
before :each do
Facter.clear
facts.each do |k, v|
Facter.stubs(:fact).with(k).returns Facter.add(k) { setcode { v } }
end
end
describe 'when validating attributes' do
[ :suffix, :provider ].each do |param|
it "should have a #{param} parameter" do
expect(described_class.attrtype(param)).to eq(:param)
end
end
[ :ensure, :backend, :directory ].each do |prop|
it "should have a #{prop} property" do
expect(described_class.attrtype(prop)).to eq(:property)
end
end
end
end
end
end
Line 1:
Load spec/spec_helper.rb
Line 4:
Loop over all supported operating systems (in metadata.json
)
Line 5:
Create a new rspec context for the operating system currently being tested
Line 7 – 11:
Stub all facts before each test
Lines 14 – 18:
Loop over each desired parameter and validate it
Line 19 – 23:
Loop over each desired property and validate it Let’s also make sure that suffix is the namevar of our type:
describe "namevar validation" do
it "should have :suffix as its namevar" do
expect(described_class.key_attributes).to eq([:suffix])
end
end
WRITE THE OPENLDAP_DATABASE TYPE
The puppet custom types live in lib/puppet/type
, so let’s create this directory first:
We can then write our puppet type to manage OpenLDAP databases in lib/puppet/type/openldap_database.rb
:
Puppet::Type.newtype(:openldap_database) do
@doc = "Manages OpenLDAP BDB and HDB databases."
ensurable
newparam(:suffix, :namevar => true) do
desc "The default namevar."
end
newproperty(:backend) do
desc "The name of the backend."
end
newproperty(:directory) do
desc "The directory where the BDB files containing this database and associated indexes live."
end
end
Let’s test:
$ bundle exec rake spec SPEC=spec/unit/puppet/type/openldap_database_spec.rb SPEC_OPTS=-fd
...
Puppet::Type::Openldap_database
on debian-7-x86_64
when validating attributes
should have a suffix parameter
should have a provider parameter
should have a ensure property
should have a backend property
should have a directory property
namevar validation
should have :suffix as its namevar
on redhat-7-x86_64
when validating attributes
should have a suffix parameter
should have a provider parameter
should have a ensure property
should have a backend property
should have a directory property
namevar validation
should have :suffix as its namevar
Finished in 0.12127 seconds (files took 0.72435 seconds to load)
12 examples, 0 failures
It Works!
Now let’s validate the attributes value. We want to ensure that ensure matches present or absent and that backend matches either bdb or hdb (the 2 current supported OpenLDAP database backends) and defaults to hdb:
describe 'when validating attribute values' do
describe 'ensure' do
[ :present, :absent ].each do |value|
it "should support #{value} as a value to ensure" do
expect { described_class.new({
:name => 'dc=example,dc=com',
:ensure => value,
})}.to_not raise_error
end
end
it "should not support other values" do
expect { described_class.new({
:name => 'dc=example,dc=com',
:ensure => 'foo',
})}.to raise_error(Puppet::Error, /Invalid value/)
end
end
describe "backend" do
[ 'bdb', 'hdb' ].each do |backend|
it "should support '#{backend}' as a value for backend" do
expect { described_class.new({
:name => 'dc=example,dc=com',
:backend => backend,
}) }.to_not raise_error
end
end
it "should default to hdb" do
expect(described_class.new({
:name => 'dc=example,dc=com'
})[:backend]).to eq :hdb
end
it "should not support other values" do
expect { described_class.new({
:name => 'dc=example,dc=com',
:backend => 'bar',
} ) }.to raise_error(Puppet::Error, /Invalid value/)
end
end
And now we can adapt our custom type:
Puppet::Type.newtype(:openldap_database) do
@doc = "Manages OpenLDAP BDB and HDB databases."
ensurable
newparam(:suffix, :namevar => true) do
desc "The default namevar."
end
newproperty(:backend) do
desc "The name of the backend."
newvalues('bdb', 'hdb')
defaultto 'hdb'
end
newproperty(:directory) do
desc "The directory where the BDB files containing this database and associated indexes live."
end
end
And launch the unit tests again:
$ bundle exec rake spec SPEC=spec/unit/puppet/type/openldap_database_spec.rb SPEC_OPTS=-fd
...
Puppet::Type::Openldap_database
on debian-7-x86_64
when validating attributes
should have a suffix parameter
should have a provider parameter
should have a ensure property
should have a backend property
should have a directory property
namevar validation
should have :suffix as its namevar
when validating attribute values
ensure
should support present as a value to ensure
should support absent as a value to ensure
should not support other values
backend
should support 'bdb' as a value for backend
should support 'hdb' as a value for backend
should default to hdb
should not support other values
on redhat-7-x86_64
when validating attributes
should have a suffix parameter
should have a provider parameter
should have a ensure property
should have a backend property
should have a directory property
namevar validation
should have :suffix as its namevar
when validating attribute values
ensure
should support present as a value to ensure
should support absent as a value to ensure
should not support other values
backend
should support 'bdb' as a value for backend
should support 'hdb' as a value for backend
should default to hdb
should not support other values
Finished in 0.30884 seconds (files took 0.73265 seconds to load)
26 examples, 0 failures
We should ideally validate more things:
- the suffix should be
/dc=[^,]+(,dc=[^,]+)*/
- the directory should be an absolute path etc.
…but this will be left as an exercise for you.
WRITE THE CUSTOM PROVIDER FOR OPENLDAP_DATABASE TYPE
Now let’s write a provider for our custom type. As said, we will use the OpenLDAP configuration API to manage the databases. So we’ll use slapcat to read the configuration and ldapmodify
to update it. Since we’ll not have a real OpenLDAP server running on our workstation where we run the tests, we’ll have to mock the commands.
Let’s start, as usual, by writing the unit tests.
WRITE THE UNIT TEST FOR THE OPENLDAP_DATABASE’S OLC PROVIDER
Unit tests for providers lives in spec/unit/puppet/provider/<type>
, so let’s create this directory first:
$ mkdir -p spec/unit/puppet/provider/openldap_database
Next, we can create the unit tests for openldap_database
’s olc
provider in spec/unit/puppet/provider/openldap_database/olc_spec.rb
:
Again, the first thing to do is to require our spec_helper.rb
file:
require 'spec_helper'
describe Puppet::Type.type(:openldap_database).provider(:olc) do
# Tests will go here
end
The first thing we want to do is to list the current instances of the OpenLDAP databases and use this list as a prefetch cache for the resources. So our provider must respond to the self.instances
and self.prefetch
methods:
require 'spec_helper'
describe Puppet::Type.type(:openldap_database).provider(:olc) do
on_supported_os.each do |os, facts|
context "on #{os}" do
before :each do
Facter.clear
facts.each do |k, v|
Facter.stubs(:fact).with(k).returns Facter.add(k) { setcode { v } }
end
end
describe 'instances' do
it 'should have an instance method' do
expect(described_class).to respond_to :instances
end
end
describe 'prefetch' do
it 'should have a prefetch method' do
expect(described_class).to respond_to :prefetch
end
end
end
end
end
WRITE THE OPENLDAP_DATABASE’S OLC PROVIDER
The puppet providers for the openldap_database
type lives in lib/puppet/provider/openldap_database
, so let’s create this directory first:
$ mkdir -p lib/puppet/provider/openldap_database
Then we can create the olc
provider for the openldap_database
type in lib/puppet/provider/openldap_database/olc.rb
with a self.instances
and a self.prefetch
methods plus some things we’ll need:
Puppet::Type.type(:openldap_database).provide(:olc) do
commands :slapcat => 'slapcat', :ldapmodify => 'ldapmodify'
mk_resource_methods
def self.instances
end
def self.prefetch
end
def exists?
@property_hash[:ensure] == :present
end
end
Line 3:
Declares commands we will use (used by confinement) and creates helper methods dynamically for each command.
Line 5:
Creates accessors (getters and setters) for each property using the @property_hash
instance variable created by the self.prefetch
method
Line 13 – 15:
Defines the exists?
method which returns a boolean based on whether the resource is prefetched.
And let’s test:
$ bundle exec rake spec SPEC=spec/unit/puppet/provider/openldap_database/olc_spec.rb SPEC_OPTS=-fd
...
Puppet::Type::Openldap_database::ProviderOlc
on debian-7-x86_64
instances
should have an instance method
prefetch
should have a prefetch method
on redhat-7-x86_64
instances
should have an instance method
prefetch
should have a prefetch method
Finished in 0.02769 seconds (files took 0.44447 seconds to load)
4 examples, 0 failures
WRITE THE UNIT TESTS FOR THE SELF.INSTANCES METHOD
Now that everything is set up, let’s write the next unit test. We want to make sure that the self.instances
method returns the right resources:
We’ll test with no database:
context 'without databases' do
before :each do
described_class.expects(:slapcat).with(
'-b', 'cn=config', '-H',
'ldap:///???(&(objectClass=olcDatabaseConfig)(|(objectClass=olcBdbConfig)(objectClass=olcHdbConfig)))'
).returns ''
end
it 'should return no resources' do
expect(described_class.instances.size).to eq(0)
end
end
Then, with one database:
context 'with one database' do
before :each do
described_class.expects(:slapcat).with(
'-b', 'cn=config', '-H',
'ldap:///???(&(objectClass=olcDatabaseConfig)(|(objectClass=olcBdbConfig)(objectClass=olcHdbConfig)))'
).returns 'dn: olcDatabase={1}hdb,cn=config
objectClass: olcHdbConfig
olcDatabase: {1}hdb
olcDbDirectory: /var/lib/ldap
olcSuffix: dc=example,dc=com
'
end
it 'should return one resource' do
expect(described_class.instances.size).to eq(1)
end
it 'should return the resource dc=example,dc=com' do
expect(described_class.instances[0].instance_variable_get("@property_hash")).to eq( {
:ensure => :present,
:name => 'dc=example,dc=com',
:suffix => 'dc=example,dc=com',
:backend => 'hdb',
:directory => '/var/lib/ldap',
} )
end
end
And finally with two databases:
context 'with two databases' do
before :each do
described_class.expects(:slapcat).with(
'-b', 'cn=config', '-H',
'ldap:///???(&(objectClass=olcDatabaseConfig)(|(objectClass=olcBdbConfig)(objectClass=olcHdbConfig)))'
).returns 'dn: olcDatabase={1}hdb,cn=config
objectClass: olcHdbConfig
olcDatabase: {1}hdb
olcDbDirectory: /var/lib/ldap1
olcSuffix: dc=foo,dc=com
dn: olcDatabase={1}bdb,cn=config
objectClass: olcBdbConfig
olcDatabase: {1}bdb
olcDbDirectory: /var/lib/ldap2
olcSuffix: dc=bar,dc=com
'
end
it 'should return two resource' do
expect(described_class.instances.size).to eq(2)
end
it 'should return the resource dc=foo,dc=com' do
expect(described_class.instances[0].instance_variable_get("@property_hash")).to eq( {
:ensure => :present,
:name => 'dc=foo,dc=com',
:suffix => 'dc=foo,dc=com',
:backend => 'hdb',
:directory => '/var/lib/ldap1',
} )
end
it 'should return the resource dc=bar,dc=com' do
expect(described_class.instances[1].instance_variable_get("@property_hash")).to eq( {
:ensure => :present,
:name => 'dc=bar,dc=com',
:suffix => 'dc=bar,dc=com',
:backend => 'bdb',
:directory => '/var/lib/ldap2',
} )
end
end
If you launch the unit tests now, it will obviously fails because self.instances
returns nil
and we try to call the size
method on it.
WRITE THE PROVIDER’S SELF.INSTANCES METHOD
Let’s write the provider code that populates the instances Array
:
Puppet::Type.type(:openldap_database).provide(:olc) do
commands :slapcat => 'slapcat', :ldapmodify => 'ldapmodify'
mk_resource_methods
def self.instances
databases = slapcat(
'-b', 'cn=config', '-H',
'ldap:///???(&(objectClass=olcDatabaseConfig)(|(objectClass=olcBdbConfig)(objectClass=olcHdbConfig)))'
)
databases.split("\n\n").collect do |paragraph|
suffix = backend = directory = nil
paragraph.split("\n").collect do |line|
case line
when /^olcDatabase: /
backend = line.match(/^olcDatabase: \{\d+\}(bdb|hdb)$/).captures[0]
when /^olcDbDirectory: /
directory = line.split(' ')[1]
when /^olcSuffix: /
suffix = line.split(' ')[1]
end
end
new({
:ensure => :present,
:name => suffix,
:suffix => suffix,
:backend => backend,
:directory => directory,
})
end
end
def self.prefetch
end
def exists?
@property_hash[:ensure] == :present
end
end
And if we test now:
$ bundle exec rake spec SPEC=spec/unit/puppet/provider/openldap_database/olc_spec.rb SPEC_OPTS=-fd
...
Puppet::Type::Openldap_database::ProviderOlc
on debian-7-x86_64
instances
should have an instance method
without databases
should return no resources
with one database
should return one resource
should return the resource dc=example,dc=com
with two databases
should return two resource
should return the resource dc=foo,dc=com
should return the resource dc=bar,dc=com
prefetch
should have a prefetch method
on redhat-7-x86_64
instances
should have an instance method
without databases
should return no resources
with one database
should return one resource
should return the resource dc=example,dc=com
with two databases
should return two resource
should return the resource dc=foo,dc=com
should return the resource dc=bar,dc=com
prefetch
should have a prefetch method
Finished in 0.11436 seconds (files took 0.45002 seconds to load)
16 examples, 0 failures
Now that we have the self.instances
method, we want to be able to create a database. We will now code the self.prefetch
method, to pass the discovered instances to the catalog resources.
THE PREFETCH METHOD
The prefetch
method is used to build a cache of the resources which can be used to easily assert the existence and synchronisation state of the resource, using the @property_hash
instance variable.
def self.prefetch(resources)
databases = instances
resources.keys.each do |name|
if provider = databases.find{ |database| database.name == name }
resources[name].provider = provider
end
end
end
Line 1:
The method takes a list of catalog resources as argument, and is expected to associate RAL resources to each of them, if they already exist.
Line 2:
Retrieve all the discovered resources from the self.instances method. This is the most common way to prefetch resources when resources can all be automatically discovered by self.instances
.
Line 4:
For each catalog resource passed to the method, look into the discovered instances to find a RAL resource with a matching name.
Line 5:
If a matching resource is found, associate it to the catalog resource. This will set the values of the @property_hash
instance variable for this resource based on the values set in the self.instances
method for this resource.
THE UNIT TEST FOR THE CREATE METHOD
Now let’s check that, when we want to create a resource, it generates a valid ldif and that the resource exists. In spec/unit/puppet/provider/openldap_database/olc_spec.rb
:
THE CREATE METHOD
def create
ldif = %Q{dn: olcDatabase=#{resource[:backend]},cn=config
changetype: add
objectClass: olc#{resource[:backend].to_s.capitalize}Config
olcDatabase: #{resource[:backend]}
olcDbDirectory: #{resource[:directory]}
olcSuffix: #{resource[:suffix]}
olcAccess: to *
by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
by * break
olcAccess: to *
by * read
}
begin
execute(%Q{echo -n "#{ldif}" | ldapmodify -Y EXTERNAL -H ldapi:///})
rescue Exception => e
raise Puppet::Error, "LDIF content:\n#{ldif}\nError message: #{e.message}"
end
ldif = %Q{dn: #{resource[:suffix]}
changetype: add
objectClass: dcObject
objectClass: organization
dc: #{resource[:suffix].split(/,?dc=/).delete_if { |c| c.empty? }[0]}
o: #{resource[:suffix].split(/,?dc=/).delete_if { |c| c.empty? }.join('.')}
}
begin
execute(%Q{echo -n "#{ldif}" | ldapmodify -Y EXTERNAL -H ldapi:///})
rescue Exception => e
raise Puppet::Error, "LDIF content:\n#{ldif}\nError message: #{e.message}"
end
@property_hash[:ensure] = :present
end
Finally, let’s run the acceptance test to see if it really works.
ON REDHAT 7
$ BEAKER_set=centos-7-x86_64-vagrant bundle exec rspec spec/acceptance/openldap_database_spec.rb
...
openldap_database
running puppet code
localhost $ scp /tmp/beaker20150325-32609-1y6r3y0 centos-7-x64:/tmp/apply_manifest.pp.VtyjcH {:ignore => }
localhost $ scp /tmp/beaker20150325-32609-1p5vwqa centos-7-x64:/tmp/apply_manifest.pp.Dr6fc7 {:ignore => }
should work with no errors
can connect with ldapsearch
Destroying vagrant boxes
==> centos-7-x64: Forcing shutdown of VM...
==> centos-7-x64: Destroying VM and associated drives...
Finished in 28.65 seconds (files took 2 minutes 0.8 seconds to load)
2 examples, 0 failures
Yes, it does!
ON DEBIAN 7
BEAKER_set=debian-7-x86_64-vagrant bundle exec rspec spec/acceptance/openldap_database_spec.rb
...
openldap_database
running puppet code
localhost $ scp /tmp/beaker20150325-3533-1uv6518 debian-7-x64:/tmp/apply_manifest.pp.wOicrY {:ignore => }
localhost $ scp /tmp/beaker20150325-3533-dby97z debian-7-x64:/tmp/apply_manifest.pp.UwTEok {:ignore => }
should work with no errors
can connect with ldapsearch
Destroying vagrant boxes
==> debian-7-x64: Forcing shutdown of VM...
==> debian-7-x64: Destroying VM and associated drives...
Finished in 36.46 seconds (files took 1 minute 48.05 seconds to load)
2 examples, 0 failures
Now that we have a functional type and provider to manage an OpenLDAP database, we will create a function to manage the database’s Root Password in the coming part IV.