Added modules ldap_upsert and ldap_search.  Wrote documentation.
				
					
				
			This commit is contained in:
		
							parent
							
								
									5b5d5098ce
								
							
						
					
					
						commit
						03bc81122d
					
				
					 7 changed files with 727 additions and 22 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
*.pyc
 | 
			
		||||
							
								
								
									
										274
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										274
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,27 +1,34 @@
 | 
			
		|||
# Ansible LDAP Modules
 | 
			
		||||
 | 
			
		||||
For whatever reasons, Ansible doesn't include modules to manipulate an
 | 
			
		||||
LDAP server in its [core](http://docs.ansible.com/ansible/modules_by_category.html).
 | 
			
		||||
LDAP server in its
 | 
			
		||||
[core](http://docs.ansible.com/ansible/modules_by_category.html).
 | 
			
		||||
 | 
			
		||||
Peter Sagerson implemented a pair of modules and has
 | 
			
		||||
[attempted to get them into Ansible](http://grokbase.com/t/gg/ansible-devel/148892tek3/ldap-modules),
 | 
			
		||||
but no luck yet.  He's instead hosted his code on
 | 
			
		||||
[Bitbucket](https://bitbucket.org/psagers/ansible-ldap).
 | 
			
		||||
 | 
			
		||||
This repository is a fork of his original work (with some cosmetic
 | 
			
		||||
improvements) hosted on GitHub.
 | 
			
		||||
This repository is a fork of his original work with some additions
 | 
			
		||||
(and cosmetic improvements) hosted on GitHub.
 | 
			
		||||
 | 
			
		||||
# Installation
 | 
			
		||||
 | 
			
		||||
You'll need to clone this repository somewhere into your Ansible (use
 | 
			
		||||
a git submodule).
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git clone https://github.com/unchained-capital/ansible-ldap-modules
 | 
			
		||||
```
 | 
			
		||||
The `python-ldap` library is required on whatever host is executing
 | 
			
		||||
these modules.  This can be confusing if you use both
 | 
			
		||||
`local_action`-type tasks and tasks which run on remote hosts and both
 | 
			
		||||
need to talk do an LDAP server -- you'll need to install `python-ldap`
 | 
			
		||||
both locally on your controller as well as remotely on the hosts you
 | 
			
		||||
run the tasks on.
 | 
			
		||||
 | 
			
		||||
The `python-ldap` library is required on the Ansible controller node
 | 
			
		||||
for these modules to load.  Install it with
 | 
			
		||||
In addition, you'll need to clone this repository itself to your
 | 
			
		||||
controller and put it somewhere `ansible` can find it.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## python-ldap Installation
 | 
			
		||||
 | 
			
		||||
Install it locally via the following commands:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ cd ansible-ldap-modules
 | 
			
		||||
| 
						 | 
				
			
			@ -34,10 +41,38 @@ You may need to install some system dependencies first:
 | 
			
		|||
$ sudo apt-get install python-dev libsasl2-dev libldap2-dev libssl-dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
For remote hosts, this can be automated via Ansible:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
# in prepare_remote_ldap_host.yml, for example
 | 
			
		||||
- name: Install dependencies
 | 
			
		||||
  apt: name="{{ item }}" state=present
 | 
			
		||||
  with_items:
 | 
			
		||||
    - python-pip
 | 
			
		||||
    - libsasl2-dev
 | 
			
		||||
    - libldap2-dev
 | 
			
		||||
    - libssl-dev
 | 
			
		||||
 | 
			
		||||
- name: Upgrade pip
 | 
			
		||||
  command: pip install --upgrade pip
 | 
			
		||||
 | 
			
		||||
- name: Install python-ldap
 | 
			
		||||
  pip: name=python-ldap state=present
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Modules Installation
 | 
			
		||||
 | 
			
		||||
You'll need to clone this repository somewhere on your controller
 | 
			
		||||
machine so that `ansible` can find it.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git clone https://github.com/unchained-capital/ansible-ldap-modules
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Once you have `python-ldap` installed, you'll need to link the
 | 
			
		||||
executable files in this repository into Ansible's library path.  The
 | 
			
		||||
simplest way to do this is to create a `library` folder at the
 | 
			
		||||
top-level of your Ansible repository and create symlinks within it to
 | 
			
		||||
top-level of your playbook repository and create symlinks within it to
 | 
			
		||||
the module files in this repository:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			@ -52,14 +87,213 @@ or the `library` entry within the `defaults` section of your
 | 
			
		|||
 | 
			
		||||
# Usage
 | 
			
		||||
 | 
			
		||||
(copied from psager's [original README](https://bitbucket.org/psagers/ansible-ldap/src/ca4c0025358cdeba33e9ef0369af3430bf4812ad/README.md))
 | 
			
		||||
Remember, if you run these tasks locally, you need `python-ldap`
 | 
			
		||||
installed locally.  If you run them on a remote machine, you need
 | 
			
		||||
`python-ldap` installed on that remote machine.
 | 
			
		||||
 | 
			
		||||
This project contains a pair of Ansible modules for manipulating an
 | 
			
		||||
LDAP directory. `ldap_entry` can be used to ensure that an entire
 | 
			
		||||
entry exists and `ldap_attr` can be used to ensure the values of an
 | 
			
		||||
entry's attributes.
 | 
			
		||||
## Shared Behavior
 | 
			
		||||
 | 
			
		||||
All the modules in this repository share some behaviors.  This section
 | 
			
		||||
describes those behaviors.  The next section describes each module in
 | 
			
		||||
detail.
 | 
			
		||||
 | 
			
		||||
### Specifying an LDAP Server
 | 
			
		||||
 | 
			
		||||
Here is a simple example of creating an entry (more details on this
 | 
			
		||||
below):
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- hosts: server0
 | 
			
		||||
  tasks:
 | 
			
		||||
  - name: Ensure an LDAP entry exists for ou=People
 | 
			
		||||
    ldap_entry:
 | 
			
		||||
      dn: "ou=People,dc=example,dc=com"
 | 
			
		||||
      ou: People
 | 
			
		||||
      objectClass: organizationalUnit
 | 
			
		||||
      description: Getting together and having a good time.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The target host of an LDAP operation is assumed to be the same host as
 | 
			
		||||
the Ansible task is executing so the above task would attempt to talk
 | 
			
		||||
to LDAP server running on `server0` listenting on port 389.  This example:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- hosts: server0
 | 
			
		||||
  tasks:
 | 
			
		||||
  - name: Ensure an LDAP entry exists for ou=People
 | 
			
		||||
    ldap_entry:
 | 
			
		||||
	  server_uri: ldapi://server1/
 | 
			
		||||
      dn: "ou=People,dc=example,dc=com"
 | 
			
		||||
      ou: People
 | 
			
		||||
      objectClass: organizationalUnit
 | 
			
		||||
      description: Getting together and having a good time.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
would target an LDAP server at `server1`.
 | 
			
		||||
 | 
			
		||||
### Choosing Credentials
 | 
			
		||||
 | 
			
		||||
Without any credentials (as in the above examples), the request will
 | 
			
		||||
be made via a SASL `EXTERNAL` bind (similar to `ldapadd ... -Y
 | 
			
		||||
EXTERNAL ...` on the command-line).
 | 
			
		||||
 | 
			
		||||
Credentials can be specified as well:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- hosts: server0
 | 
			
		||||
  tasks:
 | 
			
		||||
  - name: Ensure an LDAP entry exists for ou=People
 | 
			
		||||
    ldap_entry:
 | 
			
		||||
	  bind_dn: "cn=admin,dc=example,dc=com"
 | 
			
		||||
	  bind_pw: "password"
 | 
			
		||||
      dn: "ou=People,dc=example,dc=com"
 | 
			
		||||
      ou: People
 | 
			
		||||
      objectClass: organizationalUnit
 | 
			
		||||
      description: Getting together and having a good time.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Modules
 | 
			
		||||
 | 
			
		||||
This repository provides four different modules:
 | 
			
		||||
 | 
			
		||||
### ldap_entry
 | 
			
		||||
 | 
			
		||||
Ensures that an entry with a given `dn` exists/doesn't exist.  If
 | 
			
		||||
missing, it creates it.  It present it does nothing.  In particular,
 | 
			
		||||
even if the entry has different attributes from those specified in
 | 
			
		||||
Ansible, `ldap_entry` does nothing.  You can, however, use
 | 
			
		||||
`ldap_entry` to remove LDAP entries if you specify `state=absent`.
 | 
			
		||||
 | 
			
		||||
#### Creating an entry
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- name: Ensure an LDAP entry exists for ou=People
 | 
			
		||||
  ldap_entry:
 | 
			
		||||
    dn: "ou=People,dc=example,dc=com"
 | 
			
		||||
	ou: People
 | 
			
		||||
	objectClass: organizationalUnit
 | 
			
		||||
	description: Getting together and having a good time.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If the `ou=People,dc=example,dc=com` entry has its `description` field
 | 
			
		||||
changed, this task will not update it.  It's therefore best to only
 | 
			
		||||
specify the minimal number of fields required to successfully create
 | 
			
		||||
the entity, given its LDAP `objectClass` values and later use
 | 
			
		||||
`ldap_attr` to explicitly declare the expected state for each
 | 
			
		||||
attribute individually.  Or use `ldap_upsert`.
 | 
			
		||||
 | 
			
		||||
#### Removing an entry
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- name: Ensure an LDAP entry exists for ou=People
 | 
			
		||||
  ldap_entry:
 | 
			
		||||
    dn: "ou=People,dc=example,dc=com"
 | 
			
		||||
    state: absent
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ldap_attr
 | 
			
		||||
 | 
			
		||||
Ensures that an attribute with a given value exists/doesn't exist for
 | 
			
		||||
a given entry.  If the entry does not exist, it throws an error.  If
 | 
			
		||||
the doesn't exist, it creates it.  If it exists but has a different
 | 
			
		||||
value, it updates it.  You can use `ldap_attr` to remove LDAP
 | 
			
		||||
attributes if you specify `state=absent`.
 | 
			
		||||
 | 
			
		||||
#### Specifying an attribute exactly
 | 
			
		||||
 | 
			
		||||
Here's a simple example.
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- name: Ensure description is correct for ou=People
 | 
			
		||||
  ldap_attr:
 | 
			
		||||
    dn: "ou=People,dc=example,dc=com"
 | 
			
		||||
	name:  description
 | 
			
		||||
	state:  exact
 | 
			
		||||
	values: Getting together and having a good time.
 | 
			
		||||
 | 
			
		||||
- name: Ensure members are correct for cn=Admins,ou=Groups
 | 
			
		||||
  ldap_attr:
 | 
			
		||||
    dn: "cn=Admins,ou=Groups,dc=example,dc=com"
 | 
			
		||||
	name:   member
 | 
			
		||||
	state:  exact
 | 
			
		||||
	values:
 | 
			
		||||
	  - cn=joe,ou=People,dc=example,dc=com
 | 
			
		||||
	  - cn=bob,ou=People,dc=example,dc=com
 | 
			
		||||
```
 | 
			
		||||
This is a lot of work for many entries with many attributes.  Consider
 | 
			
		||||
`ldap_upsert` in these cases.
 | 
			
		||||
 | 
			
		||||
#### Appending to an attribute
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- name: Ensure members are present in cn=Admins,ou=Groups
 | 
			
		||||
  ldap_attr:
 | 
			
		||||
    dn: "cn=Admins,ou=Groups,dc=example,dc=com"
 | 
			
		||||
	name:   member
 | 
			
		||||
	state:  present
 | 
			
		||||
	values:
 | 
			
		||||
	  - cn=joe,ou=People,dc=example,dc=com
 | 
			
		||||
	  - cn=bob,ou=People,dc=example,dc=com
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The user `cn=mike,ou=People,dc=example,dc=com` could still be a
 | 
			
		||||
`member` of `cn=Admins,ou=Groups,dc=example,dc=com` after this task
 | 
			
		||||
runs.
 | 
			
		||||
 | 
			
		||||
#### Removing an attribute
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- name: Ensure user's password is not set
 | 
			
		||||
  ldap_attr:
 | 
			
		||||
    dn: "cn=joe,ou=People,dc=example,dc=com"
 | 
			
		||||
	state: absent
 | 
			
		||||
	name:  userPassword
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ldap_upsert
 | 
			
		||||
 | 
			
		||||
Ensures both that an entry exists as well as that its attributs have
 | 
			
		||||
particular values.  You cannot use `ldap_upsert` to remove entries or
 | 
			
		||||
their attributes, but it useful when creating lots of entities with
 | 
			
		||||
their attributes.
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
 | 
			
		||||
- name: Create a user
 | 
			
		||||
  ldap_upsert:
 | 
			
		||||
    dn:    "uid=joe1234,ou=People,dc=example,dc=com"
 | 
			
		||||
	objectClass:
 | 
			
		||||
	  - account
 | 
			
		||||
	  - posixAccount
 | 
			
		||||
	cn:    "Joe Smith"
 | 
			
		||||
	gn:    "Joe"
 | 
			
		||||
	sn:    "Smith"
 | 
			
		||||
	uid:   "joe1234"
 | 
			
		||||
	homeDirectory: "/home/joe1234"
 | 
			
		||||
	userPassword: "..."
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ldap_search
 | 
			
		||||
 | 
			
		||||
Perform an LDAP search.  Useful in combination with Ansible's
 | 
			
		||||
`register` keyword.
 | 
			
		||||
 | 
			
		||||
This example performs a task for each LDAP user `account`:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
- name: 
 | 
			
		||||
  ldap_search:
 | 
			
		||||
    base: "ou=People,dc=example,dc=com"
 | 
			
		||||
	scope: onelevel
 | 
			
		||||
	filter: "(objectClass=account)"
 | 
			
		||||
  register: ldap_user_search
 | 
			
		||||
 | 
			
		||||
- name: View search results
 | 
			
		||||
  debug: var=ldap_user_search
 | 
			
		||||
 | 
			
		||||
- name: Do something for each user
 | 
			
		||||
  # ...
 | 
			
		||||
  with_items: "{{ ldap_user_search.results }}"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Regrettably, Ansible does not have any sensible mechanism for
 | 
			
		||||
packaging and distributing third-party modules with rendered
 | 
			
		||||
documentation and runnable unit tests. The LDAP modules do have
 | 
			
		||||
complete documentation strings embedded.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										0
									
								
								__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								__init__.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -129,7 +129,7 @@ def main():
 | 
			
		|||
            'values': dict(required=True),
 | 
			
		||||
            'state': dict(default='present', choices=['present', 'absent', 'exact']),
 | 
			
		||||
            'server_uri': dict(default='ldapi:///'),
 | 
			
		||||
            'start_tls': dict(default='false', choices=BOOLEANS),
 | 
			
		||||
            'start_tls': dict(default='false', choices=(BOOLEANS+['True', True, 'False', False])),
 | 
			
		||||
            'bind_dn': dict(default=None),
 | 
			
		||||
            'bind_pw': dict(default='', no_log=True),
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,7 +95,7 @@ def main():
 | 
			
		|||
            'dn': dict(required=True),
 | 
			
		||||
            'state': dict(default='present', choices=['present', 'absent']),
 | 
			
		||||
            'server_uri': dict(default='ldapi:///'),
 | 
			
		||||
            'start_tls': dict(default='false', choices=BOOLEANS),
 | 
			
		||||
            'start_tls': dict(default='false', choices=(BOOLEANS+['True', True, 'False', False])),
 | 
			
		||||
            'bind_dn': dict(default=None),
 | 
			
		||||
            'bind_pw': dict(default='', no_log=True),
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										231
									
								
								ldap_search
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										231
									
								
								ldap_search
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,231 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
 | 
			
		||||
from traceback import format_exc
 | 
			
		||||
 | 
			
		||||
import ldap
 | 
			
		||||
import ldap.modlist
 | 
			
		||||
import ldap.sasl
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DOCUMENTATION = """
 | 
			
		||||
---
 | 
			
		||||
module: ldap_search
 | 
			
		||||
short_description: Return the results of an LDAP search.
 | 
			
		||||
description:
 | 
			
		||||
    - Return the results of an LDAP search.  Use in combination with
 | 
			
		||||
      Ansible's 'register' statement.
 | 
			
		||||
 | 
			
		||||
notes: []
 | 
			
		||||
version_added: null
 | 
			
		||||
author: Dhruv Bansal
 | 
			
		||||
requirements:
 | 
			
		||||
    - python-ldap
 | 
			
		||||
options:
 | 
			
		||||
    base:
 | 
			
		||||
        required: true
 | 
			
		||||
        description:
 | 
			
		||||
            - The base to search from.
 | 
			
		||||
    scope:
 | 
			
		||||
        required: false
 | 
			
		||||
        choices: [base, onelevel, subordinate, children]
 | 
			
		||||
        default: base
 | 
			
		||||
        description:
 | 
			
		||||
            - The LDAP scope to use when searching.
 | 
			
		||||
    filter:
 | 
			
		||||
        required: false
 | 
			
		||||
        default: '(objectClass=*)'
 | 
			
		||||
        description:
 | 
			
		||||
            - The filter to apply to the search.
 | 
			
		||||
    attrs:
 | 
			
		||||
        required: false
 | 
			
		||||
        default: none
 | 
			
		||||
        description:
 | 
			
		||||
            - A list of attrs to limit the results to.  Can be an
 | 
			
		||||
              actual list or just a comma-separated string.
 | 
			
		||||
    schema:
 | 
			
		||||
        required: false
 | 
			
		||||
        default: false
 | 
			
		||||
        description:
 | 
			
		||||
            - Return the full attribute schema of entries, not their
 | 
			
		||||
              attribute values.  Overrides C(attrs) when given.
 | 
			
		||||
    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 = """
 | 
			
		||||
# Return all entries within the 'groups' organizational unit.
 | 
			
		||||
- ldap_search: base='ou=groups,dc=example,dc=com'
 | 
			
		||||
  register: ldap_groups
 | 
			
		||||
  sudo: true
 | 
			
		||||
 | 
			
		||||
# Return GIDs for all groups
 | 
			
		||||
- ldap_entry: base='ou=groups,dc=example,dc=com' scope=onelevel attrs="gidNumber"
 | 
			
		||||
  register: ldap_group_gids
 | 
			
		||||
  sudo: true
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    module = AnsibleModule(
 | 
			
		||||
        argument_spec={
 | 
			
		||||
            'base': dict(required=True),
 | 
			
		||||
            'scope': dict(default='base', choices=['base', 'onelevel', 'subordinate', 'children']),
 | 
			
		||||
            'filter': dict(default='(objectClass=*)'),
 | 
			
		||||
            'attrs': dict(default=None),
 | 
			
		||||
            'schema': dict(default=False, choices=(BOOLEANS+['True', True, 'False', False])),
 | 
			
		||||
            'server_uri': dict(default='ldapi:///'),
 | 
			
		||||
            'start_tls': dict(default='false', choices=(BOOLEANS+['True', True, 'False', False])),
 | 
			
		||||
            'bind_dn': dict(default=None),
 | 
			
		||||
            'bind_pw': dict(default='', no_log=True),
 | 
			
		||||
        },
 | 
			
		||||
        check_invalid_arguments=False,
 | 
			
		||||
        supports_check_mode=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        LdapSearch(module).main()
 | 
			
		||||
    except ldap.LDAPError, e:
 | 
			
		||||
        module.fail_json(msg=str(e), exc=format_exc())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LdapSearch(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.base = self._utf8_param('base')
 | 
			
		||||
        self.filterstr = self._utf8_param('filter')
 | 
			
		||||
        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.attrlist = []
 | 
			
		||||
 | 
			
		||||
        self._load_scope()
 | 
			
		||||
        self._load_attrs()
 | 
			
		||||
        self._load_schema()
 | 
			
		||||
 | 
			
		||||
        # 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_schema(self):
 | 
			
		||||
        self.schema = self.module.boolean(self.module.params['schema'])
 | 
			
		||||
        if self.schema:
 | 
			
		||||
            self.attrsonly = 1
 | 
			
		||||
        else:
 | 
			
		||||
            self.attrsonly = 0
 | 
			
		||||
 | 
			
		||||
    def _load_scope(self):
 | 
			
		||||
        scope = self.module.params['scope']
 | 
			
		||||
        if   scope == 'base':        self.scope = ldap.SCOPE_BASE
 | 
			
		||||
        elif scope == 'onelevel':    self.scope = ldap.SCOPE_ONELEVEL
 | 
			
		||||
        elif scope == 'subordinate': self.scope = ldap.SCOPE_SUBORDINATE
 | 
			
		||||
        elif scope == 'children':    self.scope = ldap.SCOPE_SUBTREE
 | 
			
		||||
        else:
 | 
			
		||||
            self.module.fail_json(msg="scope must be one of: base, onelevel, subordinate, children")
 | 
			
		||||
 | 
			
		||||
    def _load_attrs(self):
 | 
			
		||||
        if self.module.params['attrs'] is None:
 | 
			
		||||
            self.attrlist = None
 | 
			
		||||
        else:
 | 
			
		||||
            attrs = self._load_attr_values(self.module.params['attrs'])
 | 
			
		||||
            if len(attrs) > 0:
 | 
			
		||||
                self.attrlist = attrs
 | 
			
		||||
            else:
 | 
			
		||||
                self.attrlist = None
 | 
			
		||||
 | 
			
		||||
    def _load_attr_values(self, 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="attrs must be a string or list of strings.")
 | 
			
		||||
 | 
			
		||||
        return 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):
 | 
			
		||||
        results = self.perform_search()
 | 
			
		||||
        self.module.exit_json(changed=True, results=results)
 | 
			
		||||
 | 
			
		||||
    def perform_search(self):
 | 
			
		||||
        try:
 | 
			
		||||
            results = self.connection.search_s(self.base, self.scope, filterstr=self.filterstr, attrlist=self.attrlist, attrsonly=self.attrsonly)
 | 
			
		||||
            if self.schema:
 | 
			
		||||
                return [dict(dn=result[0],attrs=result[1].keys()) for result in results]
 | 
			
		||||
            else:
 | 
			
		||||
                return [self._extract_entry(result[0], result[1]) for result in results]
 | 
			
		||||
        except ldap.NO_SUCH_OBJECT:
 | 
			
		||||
            self.module.fail_json(msg="Base not found: {}".format(self.base))
 | 
			
		||||
 | 
			
		||||
    def _extract_entry(self, dn, attrs):
 | 
			
		||||
        extracted = {'dn': dn}
 | 
			
		||||
        for attr, val in attrs.iteritems():
 | 
			
		||||
            if len(val) == 1:
 | 
			
		||||
                extracted[attr] = val[0]
 | 
			
		||||
            else:
 | 
			
		||||
                extracted[attr] = val
 | 
			
		||||
        return extracted
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # 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()
 | 
			
		||||
							
								
								
									
										239
									
								
								ldap_upsert
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										239
									
								
								ldap_upsert
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,239 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
 | 
			
		||||
from traceback import format_exc
 | 
			
		||||
 | 
			
		||||
import ldap
 | 
			
		||||
import ldap.modlist
 | 
			
		||||
import ldap.sasl
 | 
			
		||||
 | 
			
		||||
DOCUMENTATION = """
 | 
			
		||||
---
 | 
			
		||||
module: ldap_upsert
 | 
			
		||||
short_description: Insert or update an LDAP entry.
 | 
			
		||||
description:
 | 
			
		||||
    - Insert or update an LDAP entry. Existing entries with matching
 | 
			
		||||
      C(dn) will have their attributes updated.  Otherwise a new entry
 | 
			
		||||
      will be created with the given attributes.  This module cannot
 | 
			
		||||
      delete entries or remove attributes, see M(ldap_entry) and
 | 
			
		||||
      M(ldap_attr), respectively.
 | 
			
		||||
notes: []
 | 
			
		||||
version_added: null
 | 
			
		||||
author: Peter Sagerson
 | 
			
		||||
requirements:
 | 
			
		||||
    - python-ldap
 | 
			
		||||
options:
 | 
			
		||||
    dn:
 | 
			
		||||
        required: true
 | 
			
		||||
        description:
 | 
			
		||||
            - The DN of the entry to insert or update.
 | 
			
		||||
    objectClass:
 | 
			
		||||
        required: true
 | 
			
		||||
        description:
 | 
			
		||||
            - 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.  When updating, these values are not used.
 | 
			
		||||
    '...':
 | 
			
		||||
        required: false
 | 
			
		||||
        description:
 | 
			
		||||
            - All additional arguments are taken to be LDAP attribute
 | 
			
		||||
              names like C(objectClass), with similar lists of values.
 | 
			
		||||
              If the entry exists, these attributes will be updated if
 | 
			
		||||
              necessary.  Otherwise the newly created entry will have
 | 
			
		||||
              these attributes.
 | 
			
		||||
    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 description="My Org Unit"
 | 
			
		||||
  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
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    module = AnsibleModule(
 | 
			
		||||
        argument_spec={
 | 
			
		||||
            'dn': dict(required=True),
 | 
			
		||||
            'server_uri': dict(default='ldapi:///'),
 | 
			
		||||
            'start_tls': dict(default='false', choices=(BOOLEANS+['True', True, 'False', False])),
 | 
			
		||||
            'bind_dn': dict(default=None),
 | 
			
		||||
            'bind_pw': dict(default='', no_log=True),
 | 
			
		||||
        },
 | 
			
		||||
        check_invalid_arguments=False,
 | 
			
		||||
        supports_check_mode=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        LdapUpsert(module).main()
 | 
			
		||||
    except ldap.LDAPError, e:
 | 
			
		||||
        module.fail_json(msg=str(e), exc=format_exc())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LdapUpsert(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, module):
 | 
			
		||||
        self.module = module
 | 
			
		||||
 | 
			
		||||
        self.server_uri = self.module.params['server_uri']
 | 
			
		||||
        self.start_tls  = self._boolean_param('start_tls')
 | 
			
		||||
        self.bind_dn    = self._utf8_param('bind_dn')
 | 
			
		||||
        self.bind_pw    = self._utf8_param('bind_pw')
 | 
			
		||||
        
 | 
			
		||||
        self.dn = self._utf8_param('dn')
 | 
			
		||||
        self._load_attrs()
 | 
			
		||||
 | 
			
		||||
        if 'objectClass' not in self.attrs:
 | 
			
		||||
            self.module.fail_json(msg="At least one objectClass must be provided")
 | 
			
		||||
 | 
			
		||||
    def _boolean_param(self, name):
 | 
			
		||||
        return self.module.boolean(self.module.params[name])
 | 
			
		||||
        
 | 
			
		||||
    def _utf8_param(self, name):
 | 
			
		||||
        return self._force_utf8(self.module.params[name])
 | 
			
		||||
 | 
			
		||||
    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 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 _load_attrs(self):
 | 
			
		||||
        self.attrs = {}
 | 
			
		||||
        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 main(self):
 | 
			
		||||
        if self.entry_exists():
 | 
			
		||||
            results = self.update_entry()
 | 
			
		||||
            self.module.exit_json(**results)
 | 
			
		||||
        else:
 | 
			
		||||
            insert_result = self.insert_entry()
 | 
			
		||||
            self.module.exit_json(changed=True, results=[insert_result])
 | 
			
		||||
 | 
			
		||||
    def entry_exists(self):
 | 
			
		||||
        try:
 | 
			
		||||
            results = self.connection.search_s(self.dn, ldap.SCOPE_BASE)
 | 
			
		||||
            for result in results:
 | 
			
		||||
                if result[0] == self.dn:
 | 
			
		||||
                    return True
 | 
			
		||||
        except ldap.NO_SUCH_OBJECT:
 | 
			
		||||
            return False
 | 
			
		||||
    
 | 
			
		||||
    def insert_entry(self):
 | 
			
		||||
        modlist = ldap.modlist.addModlist(self.attrs)
 | 
			
		||||
        result  = self.connection.add_s(self.dn, modlist)
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def update_entry(self):
 | 
			
		||||
        results = []
 | 
			
		||||
        for attr, value in self.attrs.iteritems():
 | 
			
		||||
            if attr == 'objectClass': continue
 | 
			
		||||
            value   = self._extract_value(value)
 | 
			
		||||
            check   = self._attribute_value_check(attr, value)
 | 
			
		||||
            if check is False:
 | 
			
		||||
                op = ldap.MOD_REPLACE
 | 
			
		||||
            elif check is None:
 | 
			
		||||
                op = ldap.MOD_ADD
 | 
			
		||||
            else:
 | 
			
		||||
                op = None       # Nothing to see here...
 | 
			
		||||
            if op is not None:
 | 
			
		||||
                result = self.connection.modify_s(self.dn, [(op, attr, value)])
 | 
			
		||||
                results.append(result)
 | 
			
		||||
        if len(results) == 0:
 | 
			
		||||
            return dict(changed=False)
 | 
			
		||||
        else:
 | 
			
		||||
            return dict(changed=True, results=results)
 | 
			
		||||
 | 
			
		||||
    def _attribute_value_check(self, attr, value):
 | 
			
		||||
        try:
 | 
			
		||||
            return bool(self.connection.compare_s(self.dn, attr, value))
 | 
			
		||||
        except ldap.NO_SUCH_ATTRIBUTE:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def _extract_value(self, values):
 | 
			
		||||
        if isinstance(values, basestring):
 | 
			
		||||
            if values == '':
 | 
			
		||||
                values = []
 | 
			
		||||
            else:
 | 
			
		||||
                values = [values]
 | 
			
		||||
 | 
			
		||||
        if not (isinstance(values, list) and all(isinstance(value, basestring) for value in values)):
 | 
			
		||||
            self.module.fail_json(msg="Attribute values must be strings or lists of strings.")
 | 
			
		||||
 | 
			
		||||
        values = map(self._force_utf8, values)
 | 
			
		||||
        if len(values) == 1:
 | 
			
		||||
            return values[0]
 | 
			
		||||
        else:
 | 
			
		||||
            return values
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # LDAP Connection
 | 
			
		||||
    #
 | 
			
		||||
    _connection = None
 | 
			
		||||
 | 
			
		||||
    @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()
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue