Skip to main content

Chef It Up: OpenDJ

Recently I had a pleasure of automating the installation of OpenDJ, a ForgeRock Directory (think LDAP) project. While I can't share the cookbook we ended up using, I can provide the most important part.

Glossary

OpenDJ

A project by ForgeRock implementing LDAPv3 server and client. This is a fork of abandoned Sun OpenDS.

ldapsearch

A command line application allowing you to search LDAP directory.

ldapmodify

A command line application that modifies the contents of LDAP directory record(s)

LWRP

You all know LWRPs are good for you. Yet, lots of people writing Chef cookbooks tend to ignore that and come up with something like this:

execute 'run ldapmodify' do
    command <<-eos
        ldapmodify blah
        echo > /tmp/.ldapmodify-ran
    eos
    not_if { ::File.exist? '/tmp/.ldapmodify-ran' }
end

This is bad because...

  • In case you modify anything in the referenced command, your execute block will not run, because of the guard file.

  • Every time you need to add another ldapmodify command, you will need to come up with another guard name.

  • It enforces idempotency not based on the data, but the side effect. And when you converge the node for the second time after removing the data, you won't get it back.

At some point you will have something like this:

/tmp/.ldapmodify-ran
/tmp/.ldapmodify-update1
/tmp/.ldapmodify-squirrel

This does not scale well.

Well, initial configuration of OpenDJ really requires something like the execute block above - the installer can run only once, so there should be a guard on the configuration files it creates.

What to do then?

Add ldapmodify resource!

You will need to create a library and a resource/provider pair. The library will implement the ldapsearch shellout, while provider will simply call it. Having the code in a library allows you to turn that ugly execute block into a less ugly opendj_ldapmodify one:

opendj_ldapmodify '/path/to.ldif' do
    only_if { !ldapsearch('ou=something').include?('ou: something') }
end

ldapsearch returns error when it can't connect to the server/baseDN is wrong, yet it returns 0 when there are no results. That makes perfect sense, but since we need a flexible call, we simply analyze the output to see whether it contained the object we were looking for.

I don't consider myself to be a ruby developer, so take the code with a grain of salt:

module OpenDjCookbook
  module Helper
    include Chef::Mixin::ShellOut

    def ldapsearch(query)
      config = node['opendj']
      cmdline = [
        '/path/to/ldapsearch',
        '-b', config['basedn'],
        '-h', config['host'],
        '-p', config['port'], # make sure this is a string
        '-D', config['userdn'],
        '-w', config['password'],
        query
      ]

      begin
        shell_out!(cmdline, :user => config['service_user']).stdout.strip
      rescue Mixlib::ShellOut::ShellCommandFailed => e
        ''
      end
    end
  end
end

-p argument needs to be a string because of mixlib-shellout#90, and yes, I am using password from a node attribute (use run_state instead!). In case of error we simply return an empty string to make not_if/only_if simpler.

Provider

ldapmodify provider is quite similar, we just shellout to ldapmodify. We assume the path to file will be the resource name, so we just do this:

use_inline_resource

action :run do
  path = new_resource.path

  execute "ldapmodify -f #{path}" do
    config = node['opendj']
    command [
      '/path/to/ldapmodify',
      '-a', # default action: add
      '-h', config['host'],
      '-p', config['port'].to_s, # must be string
      '-D', config['userdn'],
      '-w', config['password'],
      '-f', path

    ]
    user config['service_user']
  end
end

Resource

And resource is very simple:

actions: run
default_action :run

attribute :path, :name_attribute => true, :kind_of => String, :required => true

Hooking up a library to resource

You won't be able to use ldapsearch if you don't include the library into a resource, and I found that linking it in the recipe works for me, so...

# opendj/recipes/default.rb
Chef::Resource::OpendjLdapmodify.send(:include, OpendjCookbook::Helper)

# rest of the recipe

Done

So now we can call ldapmodify from chef as a resource, and use ldapsearch to avoid modifying the data store when we don't need to.

OpenDJ is quite an interesting project, you may think that LDAP is dead, but it is alive and kicking. A lot of companies use ForgeRock solutions for identity and access management and the identity part most likely lives in a LDAP database. Apache Directory Project is another implementation of LDAPv3 server, an Eclipse-based Directory Studio for manipulating the data.