#!/usr/bin/env python from traceback import format_exc import ldap import ldap.modlist import ldap.sasl DOCUMENTATION = """ --- module: ldap_entry short_description: Add or remove LDAP entries. description: - Add or remove LDAP entries. This module only asserts the existence or non-existence of an LDAP entry, not its attributes. To assert the attribute values of an entry, see M(ldap_attr). notes: [] version_added: null author: Peter Sagerson requirements: - python-ldap options: dn: required: true description: - The DN of the entry to add or remove. state: required: false choices: [present, absent] default: present description: - The target state of the entry. objectClass: required: false description: - If C(state=present), this must be a list of objectClass values to use when creating the entry. It can either be a string containing a comma-separated list of values, or an actual list of strings. '...': required: false description: - If C(state=present), all additional arguments are taken to be LDAP attribute names like C(objectClass), with similar lists of values. These should only be used to provide the minimum attributes necessary for creating an entry; existing entries are never modified. To assert specific attribute values on an existing entry, see M(ldap_attr). server_uri: required: false default: ldapi:/// description: - A URI to the LDAP server. The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location. start_tls: required: false default: false description: - If true, we'll use the START_TLS LDAP extension. bind_dn: required: false description: - A DN to bind with. If this is omitted, we'll try a SASL bind with the EXTERNAL mechanism. If this is blank, we'll use an anonymous bind. bind_pw: required: false description: - The password to use with C(bind_dn). """ EXAMPLES = """ # Make sure we have a parent entry for users. - ldap_entry: dn='ou=users,dc=example,dc=com' objectClass=organizationalUnit sudo: true # Make sure we have an admin user. - ldap_entry: dn: 'cn=admin,dc=example,dc=com' objectClass: simpleSecurityObject,organizationalRole description: An LDAP administrator userPassword: '{SSHA}pedsA5Y9wHbZ5R90pRdxTEZmn6qvPdzm' sudo: true # Get rid of an old entry. - ldap_entry: dn='ou=stuff,dc=example,dc=com' state=absent server_uri='ldap://localhost/' bind_dn='cn=admin,dc=example,dc=com' bind_pw=password """ def main(): module = AnsibleModule( argument_spec={ 'dn': dict(required=True), 'state': dict(default='present', choices=['present', 'absent']), 'server_uri': dict(default='ldapi:///'), 'start_tls': dict(default='false', choices=(list(BOOLEANS)+['True', True, 'False', False])), 'bind_dn': dict(default=None), 'bind_pw': dict(default='', no_log=True), }, check_invalid_arguments=False, supports_check_mode=True, ) try: LdapEntry(module).main() except ldap.LDAPError, e: module.fail_json(msg=str(e), exc=format_exc()) class LdapEntry(object): _connection = None def __init__(self, module): self.module = module # python-ldap doesn't understand unicode strings. Parameters that are # just going to get passed to python-ldap APIs are stored as utf-8. self.dn = self._utf8_param('dn') self.state = self.module.params['state'] self.server_uri = self.module.params['server_uri'] self.start_tls = self.module.boolean(self.module.params['start_tls']) self.bind_dn = self._utf8_param('bind_dn') self.bind_pw = self._utf8_param('bind_pw') self.attrs = {} self._load_attrs() if (self.state == 'present') and ('objectClass' not in self.attrs): self.module.fail_json(msg="When state=present, at least one objectClass must be provided") def _utf8_param(self, name): return self._force_utf8(self.module.params[name]) def _load_attrs(self): for name, raw in self.module.params.iteritems(): if name not in self.module.argument_spec: self.attrs[name] = self._load_attr_values(name, raw) def _load_attr_values(self, name, raw): if isinstance(raw, basestring): values = raw.split(',') else: values = raw if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)): self.module.fail_json(msg="{} must be a string or list of strings.".format(name)) return list(map(self._force_utf8, values)) def _force_utf8(self, value): """ If value is unicode, encode to utf-8. """ if isinstance(value, unicode): value = value.encode('utf-8') return value def main(self): if self.state == 'present': action = self.handle_present() elif self.state == 'absent': action = self.handle_absent() else: action = None if (action is not None) and (not self.module.check_mode): action() self.module.exit_json(changed=(action is not None)) # # State Implementations # def handle_present(self): """ If self.dn does not exist, returns a callable that will add it. """ if not self.is_entry_present(): modlist = ldap.modlist.addModlist(self.attrs) action = lambda: self.connection.add_s(self.dn, modlist) else: action = None return action def handle_absent(self): """ If self.dn exists, returns a callable that will delete it. """ if self.is_entry_present(): action = lambda: self.connection.delete_s(self.dn) else: action = None return action # # Util # def is_entry_present(self): try: self.connection.search_s(self.dn, ldap.SCOPE_BASE) except ldap.NO_SUCH_OBJECT: is_present = False else: is_present = True return is_present # # LDAP Connection # @property def connection(self): """ An authenticated connection to the LDAP server (cached). """ if self._connection is None: self._connection = self._connect_to_ldap() return self._connection def _connect_to_ldap(self): connection = ldap.initialize(self.server_uri) if self.start_tls: connection.start_tls_s() if self.bind_dn is not None: connection.simple_bind_s(self.bind_dn, self.bind_pw) else: connection.sasl_interactive_bind_s('', ldap.sasl.external()) return connection from ansible.module_utils.basic import * # noqa main()