We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
ldap_entry: recursive deletion
### Summary

I would like to delete a whole branch of my ldap tree with `ldap_entry` but it seems recursive deletions are not supported.

I suggest handling recursive deletions.

### Issue Type

Feature Idea

### Component Name

ldap_entry

### Additional Information

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: delete all the users
  ldap_entry:
    server_uri: "{{ ldap_api_url }}"
    bind_dn: "{{ ldap_bind_id }}"
    bind_pw: "{{ ldap_admin_password }}"
    dn: "{{ ldap_users_base }}"
    state: absent
```
```python
The full traceback is:
Traceback (most recent call last):
  File "master:~/.ansible/collections/ansible_collections/community/general/plugins/modules/ldap_entry.py", line 229, in main
  File "master:~/.ansible/collections/ansible_collections/community/general/plugins/modules/ldap_entry.py", line 166, in _delete
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 560, in delete_s
    return self.delete_ext_s(dn,None,None)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 553, in delete_ext_s
    resp_type, resp_data, resp_msgid, resp_ctrls = self.result3(msgid,all=1,timeout=self.timeout)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 748, in result3
    resp_type, resp_data, resp_msgid, decoded_resp_ctrls, retoid, retval = self.result4(
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 758, in result4
    ldap_result = self._ldap_call(self._l.result4,msgid,all,timeout,add_ctrls,add_intermediates,add_extop)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 331, in _ldap_call
    reraise(exc_type, exc_value, exc_traceback)
  File "/usr/lib/python3/dist-packages/ldap/compat.py", line 44, in reraise
    raise exc_value
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 315, in _ldap_call
    result = func(*args,**kwargs)
ldap.NOT_ALLOWED_ON_NONLEAF: {'desc': 'Operation not allowed on non-leaf', 'info': 'subordinate objects must be deleted first'}
fatal: [brumaire.yaal.coop]: FAILED! => {
    "changed": false,
    "details": "{'desc': 'Operation not allowed on non-leaf', 'info': 'subordinate objects must be deleted first'}",
    "invocation": {
        "module_args": {
            "attributes": {
                "objectClass": null
            },
            "bind_dn": "cn=admin,dc=mydomain,dc=tld",
            "bind_pw": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "dn": "ou=Users,dc=mydomain,dc=tld",
            "objectClass": null,
            "params": null,
            "server_uri": "ldapi:///",
            "start_tls": false,
            "state": "absent",
            "validate_certs": true
        }
    },
    "msg": "Entry action failed."
```

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `plugins/modules/net_tools/ldap/ldap_entry.py`
Content:
```
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 
4 # Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
5 # Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
6 #
7 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
8 
9 from __future__ import absolute_import, division, print_function
10 __metaclass__ = type
11 
12 
13 DOCUMENTATION = '''
14 ---
15 module: ldap_entry
16 short_description: Add or remove LDAP entries.
17 description:
18   - Add or remove LDAP entries. This module only asserts the existence or
19     non-existence of an LDAP entry, not its attributes. To assert the
20     attribute values of an entry, see M(community.general.ldap_attrs).
21 notes:
22   - The default authentication settings will attempt to use a SASL EXTERNAL
23     bind over a UNIX domain socket. This works well with the default Ubuntu
24     install for example, which includes a cn=peercred,cn=external,cn=auth ACL
25     rule allowing root to modify the server configuration. If you need to use
26     a simple bind to access your server, pass the credentials in I(bind_dn)
27     and I(bind_pw).
28 author:
29   - Jiri Tyr (@jtyr)
30 requirements:
31   - python-ldap
32 options:
33   attributes:
34     description:
35       - If I(state=present), attributes necessary to create an entry. Existing
36         entries are never modified. To assert specific attribute values on an
37         existing entry, use M(community.general.ldap_attrs) module instead.
38     type: dict
39   objectClass:
40     description:
41       - If I(state=present), value or list of values to use when creating
42         the entry. It can either be a string or an actual list of
43         strings.
44     type: list
45     elements: str
46   state:
47     description:
48       - The target state of the entry.
49     choices: [present, absent]
50     default: present
51     type: str
52 extends_documentation_fragment:
53 - community.general.ldap.documentation
54 
55 '''
56 
57 
58 EXAMPLES = """
59 - name: Make sure we have a parent entry for users
60   community.general.ldap_entry:
61     dn: ou=users,dc=example,dc=com
62     objectClass: organizationalUnit
63 
64 - name: Make sure we have an admin user
65   community.general.ldap_entry:
66     dn: cn=admin,dc=example,dc=com
67     objectClass:
68       - simpleSecurityObject
69       - organizationalRole
70     attributes:
71       description: An LDAP administrator
72       userPassword: "{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND"
73 
74 - name: Get rid of an old entry
75   community.general.ldap_entry:
76     dn: ou=stuff,dc=example,dc=com
77     state: absent
78     server_uri: ldap://localhost/
79     bind_dn: cn=admin,dc=example,dc=com
80     bind_pw: password
81 
82 #
83 # The same as in the previous example but with the authentication details
84 # stored in the ldap_auth variable:
85 #
86 # ldap_auth:
87 #   server_uri: ldap://localhost/
88 #   bind_dn: cn=admin,dc=example,dc=com
89 #   bind_pw: password
90 #
91 # In the example below, 'args' is a task keyword, passed at the same level as the module
92 - name: Get rid of an old entry
93   community.general.ldap_entry:
94     dn: ou=stuff,dc=example,dc=com
95     state: absent
96   args: "{{ ldap_auth }}"
97 """
98 
99 
100 RETURN = """
101 # Default return values
102 """
103 
104 import traceback
105 
106 from ansible.module_utils.basic import AnsibleModule, missing_required_lib
107 from ansible.module_utils.common.text.converters import to_native, to_bytes
108 from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs
109 
110 LDAP_IMP_ERR = None
111 try:
112     import ldap.modlist
113 
114     HAS_LDAP = True
115 except ImportError:
116     LDAP_IMP_ERR = traceback.format_exc()
117     HAS_LDAP = False
118 
119 
120 class LdapEntry(LdapGeneric):
121     def __init__(self, module):
122         LdapGeneric.__init__(self, module)
123 
124         # Shortcuts
125         self.state = self.module.params['state']
126 
127         # Add the objectClass into the list of attributes
128         self.module.params['attributes']['objectClass'] = (
129             self.module.params['objectClass'])
130 
131         # Load attributes
132         if self.state == 'present':
133             self.attrs = self._load_attrs()
134 
135     def _load_attrs(self):
136         """ Turn attribute's value to array. """
137         attrs = {}
138 
139         for name, value in self.module.params['attributes'].items():
140             if isinstance(value, list):
141                 attrs[name] = list(map(to_bytes, value))
142             else:
143                 attrs[name] = [to_bytes(value)]
144 
145         return attrs
146 
147     def add(self):
148         """ If self.dn does not exist, returns a callable that will add it. """
149         def _add():
150             self.connection.add_s(self.dn, modlist)
151 
152         if not self._is_entry_present():
153             modlist = ldap.modlist.addModlist(self.attrs)
154             action = _add
155         else:
156             action = None
157 
158         return action
159 
160     def delete(self):
161         """ If self.dn exists, returns a callable that will delete it. """
162         def _delete():
163             self.connection.delete_s(self.dn)
164 
165         if self._is_entry_present():
166             action = _delete
167         else:
168             action = None
169 
170         return action
171 
172     def _is_entry_present(self):
173         try:
174             self.connection.search_s(self.dn, ldap.SCOPE_BASE)
175         except ldap.NO_SUCH_OBJECT:
176             is_present = False
177         else:
178             is_present = True
179 
180         return is_present
181 
182 
183 def main():
184     module = AnsibleModule(
185         argument_spec=gen_specs(
186             attributes=dict(default={}, type='dict'),
187             objectClass=dict(type='list', elements='str'),
188             state=dict(default='present', choices=['present', 'absent']),
189         ),
190         required_if=[('state', 'present', ['objectClass'])],
191         supports_check_mode=True,
192     )
193 
194     if not HAS_LDAP:
195         module.fail_json(msg=missing_required_lib('python-ldap'),
196                          exception=LDAP_IMP_ERR)
197 
198     state = module.params['state']
199 
200     # Instantiate the LdapEntry object
201     ldap = LdapEntry(module)
202 
203     # Get the action function
204     if state == 'present':
205         action = ldap.add()
206     elif state == 'absent':
207         action = ldap.delete()
208 
209     # Perform the action
210     if action is not None and not module.check_mode:
211         try:
212             action()
213         except Exception as e:
214             module.fail_json(msg="Entry action failed.", details=to_native(e), exception=traceback.format_exc())
215 
216     module.exit_json(changed=(action is not None))
217 
218 
219 if __name__ == '__main__':
220     main()
221
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/plugins/modules/net_tools/ldap/ldap_entry.py b/plugins/modules/net_tools/ldap/ldap_entry.py
--- a/plugins/modules/net_tools/ldap/ldap_entry.py
+++ b/plugins/modules/net_tools/ldap/ldap_entry.py
@@ -49,6 +49,13 @@
     choices: [present, absent]
     default: present
     type: str
+  recursive:
+    description:
+      - If I(state=delete), a flag indicating whether a single entry or the
+        whole branch must be deleted.
+    type: bool
+    default: false
+    version_added: 4.6.0
 extends_documentation_fragment:
 - community.general.ldap.documentation
 
@@ -110,6 +117,7 @@
 LDAP_IMP_ERR = None
 try:
     import ldap.modlist
+    import ldap.controls
 
     HAS_LDAP = True
 except ImportError:
@@ -123,6 +131,7 @@
 
         # Shortcuts
         self.state = self.module.params['state']
+        self.recursive = self.module.params['recursive']
 
         # Add the objectClass into the list of attributes
         self.module.params['attributes']['objectClass'] = (
@@ -158,12 +167,29 @@
         return action
 
     def delete(self):
-        """ If self.dn exists, returns a callable that will delete it. """
+        """ If self.dn exists, returns a callable that will delete either
+        the item itself if the recursive option is not set or the whole branch
+        if it is. """
         def _delete():
             self.connection.delete_s(self.dn)
 
+        def _delete_recursive():
+            """ Attempt recurive deletion using the subtree-delete control.
+            If that fails, do it manually. """
+            try:
+                subtree_delete = ldap.controls.ValueLessRequestControl('1.2.840.113556.1.4.805')
+                self.connection.delete_ext_s(self.dn, serverctrls=[subtree_delete])
+            except ldap.NOT_ALLOWED_ON_NONLEAF:
+                search = self.connection.search_s(self.dn, ldap.SCOPE_SUBTREE, attrlist=('dn',))
+                search.reverse()
+                for entry in search:
+                    self.connection.delete_s(entry[0])
+
         if self._is_entry_present():
-            action = _delete
+            if self.recursive:
+                action = _delete_recursive
+            else:
+                action = _delete
         else:
             action = None
 
@@ -186,6 +212,7 @@
             attributes=dict(default={}, type='dict'),
             objectClass=dict(type='list', elements='str'),
             state=dict(default='present', choices=['present', 'absent']),
+            recursive=dict(default=False, type='bool'),
         ),
         required_if=[('state', 'present', ['objectClass'])],
         supports_check_mode=True,

{"golden_diff": "diff --git a/plugins/modules/net_tools/ldap/ldap_entry.py b/plugins/modules/net_tools/ldap/ldap_entry.py\n--- a/plugins/modules/net_tools/ldap/ldap_entry.py\n+++ b/plugins/modules/net_tools/ldap/ldap_entry.py\n@@ -49,6 +49,13 @@\n     choices: [present, absent]\n     default: present\n     type: str\n+  recursive:\n+    description:\n+      - If I(state=delete), a flag indicating whether a single entry or the\n+        whole branch must be deleted.\n+    type: bool\n+    default: false\n+    version_added: 4.6.0\n extends_documentation_fragment:\n - community.general.ldap.documentation\n \n@@ -110,6 +117,7 @@\n LDAP_IMP_ERR = None\n try:\n     import ldap.modlist\n+    import ldap.controls\n \n     HAS_LDAP = True\n except ImportError:\n@@ -123,6 +131,7 @@\n \n         # Shortcuts\n         self.state = self.module.params['state']\n+        self.recursive = self.module.params['recursive']\n \n         # Add the objectClass into the list of attributes\n         self.module.params['attributes']['objectClass'] = (\n@@ -158,12 +167,29 @@\n         return action\n \n     def delete(self):\n-        \"\"\" If self.dn exists, returns a callable that will delete it. \"\"\"\n+        \"\"\" If self.dn exists, returns a callable that will delete either\n+        the item itself if the recursive option is not set or the whole branch\n+        if it is. \"\"\"\n         def _delete():\n             self.connection.delete_s(self.dn)\n \n+        def _delete_recursive():\n+            \"\"\" Attempt recurive deletion using the subtree-delete control.\n+            If that fails, do it manually. \"\"\"\n+            try:\n+                subtree_delete = ldap.controls.ValueLessRequestControl('1.2.840.113556.1.4.805')\n+                self.connection.delete_ext_s(self.dn, serverctrls=[subtree_delete])\n+            except ldap.NOT_ALLOWED_ON_NONLEAF:\n+                search = self.connection.search_s(self.dn, ldap.SCOPE_SUBTREE, attrlist=('dn',))\n+                search.reverse()\n+                for entry in search:\n+                    self.connection.delete_s(entry[0])\n+\n         if self._is_entry_present():\n-            action = _delete\n+            if self.recursive:\n+                action = _delete_recursive\n+            else:\n+                action = _delete\n         else:\n             action = None\n \n@@ -186,6 +212,7 @@\n             attributes=dict(default={}, type='dict'),\n             objectClass=dict(type='list', elements='str'),\n             state=dict(default='present', choices=['present', 'absent']),\n+            recursive=dict(default=False, type='bool'),\n         ),\n         required_if=[('state', 'present', ['objectClass'])],\n         supports_check_mode=True,\n", "issue": "ldap_entry: recursive deletion\n### Summary\n\nI would like to delete a whole branch of my ldap tree with `ldap_entry` but it seems recursive deletions are not supported.\r\n\r\nI suggest handling recursive deletions.\n\n### Issue Type\n\nFeature Idea\n\n### Component Name\n\nldap_entry\n\n### Additional Information\n\n<!--- Paste example playbooks or commands between quotes below -->\r\n```yaml (paste below)\r\n- name: delete all the users\r\n  ldap_entry:\r\n    server_uri: \"{{ ldap_api_url }}\"\r\n    bind_dn: \"{{ ldap_bind_id }}\"\r\n    bind_pw: \"{{ ldap_admin_password }}\"\r\n    dn: \"{{ ldap_users_base }}\"\r\n    state: absent\r\n```\r\n```python\r\nThe full traceback is:\r\nTraceback (most recent call last):\r\n  File \"master:~/.ansible/collections/ansible_collections/community/general/plugins/modules/ldap_entry.py\", line 229, in main\r\n  File \"master:~/.ansible/collections/ansible_collections/community/general/plugins/modules/ldap_entry.py\", line 166, in _delete\r\n  File \"/usr/lib/python3/dist-packages/ldap/ldapobject.py\", line 560, in delete_s\r\n    return self.delete_ext_s(dn,None,None)\r\n  File \"/usr/lib/python3/dist-packages/ldap/ldapobject.py\", line 553, in delete_ext_s\r\n    resp_type, resp_data, resp_msgid, resp_ctrls = self.result3(msgid,all=1,timeout=self.timeout)\r\n  File \"/usr/lib/python3/dist-packages/ldap/ldapobject.py\", line 748, in result3\r\n    resp_type, resp_data, resp_msgid, decoded_resp_ctrls, retoid, retval = self.result4(\r\n  File \"/usr/lib/python3/dist-packages/ldap/ldapobject.py\", line 758, in result4\r\n    ldap_result = self._ldap_call(self._l.result4,msgid,all,timeout,add_ctrls,add_intermediates,add_extop)\r\n  File \"/usr/lib/python3/dist-packages/ldap/ldapobject.py\", line 331, in _ldap_call\r\n    reraise(exc_type, exc_value, exc_traceback)\r\n  File \"/usr/lib/python3/dist-packages/ldap/compat.py\", line 44, in reraise\r\n    raise exc_value\r\n  File \"/usr/lib/python3/dist-packages/ldap/ldapobject.py\", line 315, in _ldap_call\r\n    result = func(*args,**kwargs)\r\nldap.NOT_ALLOWED_ON_NONLEAF: {'desc': 'Operation not allowed on non-leaf', 'info': 'subordinate objects must be deleted first'}\r\nfatal: [brumaire.yaal.coop]: FAILED! => {\r\n    \"changed\": false,\r\n    \"details\": \"{'desc': 'Operation not allowed on non-leaf', 'info': 'subordinate objects must be deleted first'}\",\r\n    \"invocation\": {\r\n        \"module_args\": {\r\n            \"attributes\": {\r\n                \"objectClass\": null\r\n            },\r\n            \"bind_dn\": \"cn=admin,dc=mydomain,dc=tld\",\r\n            \"bind_pw\": \"VALUE_SPECIFIED_IN_NO_LOG_PARAMETER\",\r\n            \"dn\": \"ou=Users,dc=mydomain,dc=tld\",\r\n            \"objectClass\": null,\r\n            \"params\": null,\r\n            \"server_uri\": \"ldapi:///\",\r\n            \"start_tls\": false,\r\n            \"state\": \"absent\",\r\n            \"validate_certs\": true\r\n        }\r\n    },\r\n    \"msg\": \"Entry action failed.\"\r\n```\n\n### Code of Conduct\n\n- [X] I agree to follow the Ansible Code of Conduct\n", "before_files": [{"content": "#!/usr/bin/python\n# -*- coding: utf-8 -*-\n\n# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>\n# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>\n#\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import absolute_import, division, print_function\n__metaclass__ = type\n\n\nDOCUMENTATION = '''\n---\nmodule: ldap_entry\nshort_description: Add or remove LDAP entries.\ndescription:\n  - Add or remove LDAP entries. This module only asserts the existence or\n    non-existence of an LDAP entry, not its attributes. To assert the\n    attribute values of an entry, see M(community.general.ldap_attrs).\nnotes:\n  - The default authentication settings will attempt to use a SASL EXTERNAL\n    bind over a UNIX domain socket. This works well with the default Ubuntu\n    install for example, which includes a cn=peercred,cn=external,cn=auth ACL\n    rule allowing root to modify the server configuration. If you need to use\n    a simple bind to access your server, pass the credentials in I(bind_dn)\n    and I(bind_pw).\nauthor:\n  - Jiri Tyr (@jtyr)\nrequirements:\n  - python-ldap\noptions:\n  attributes:\n    description:\n      - If I(state=present), attributes necessary to create an entry. Existing\n        entries are never modified. To assert specific attribute values on an\n        existing entry, use M(community.general.ldap_attrs) module instead.\n    type: dict\n  objectClass:\n    description:\n      - If I(state=present), value or list of values to use when creating\n        the entry. It can either be a string or an actual list of\n        strings.\n    type: list\n    elements: str\n  state:\n    description:\n      - The target state of the entry.\n    choices: [present, absent]\n    default: present\n    type: str\nextends_documentation_fragment:\n- community.general.ldap.documentation\n\n'''\n\n\nEXAMPLES = \"\"\"\n- name: Make sure we have a parent entry for users\n  community.general.ldap_entry:\n    dn: ou=users,dc=example,dc=com\n    objectClass: organizationalUnit\n\n- name: Make sure we have an admin user\n  community.general.ldap_entry:\n    dn: cn=admin,dc=example,dc=com\n    objectClass:\n      - simpleSecurityObject\n      - organizationalRole\n    attributes:\n      description: An LDAP administrator\n      userPassword: \"{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND\"\n\n- name: Get rid of an old entry\n  community.general.ldap_entry:\n    dn: ou=stuff,dc=example,dc=com\n    state: absent\n    server_uri: ldap://localhost/\n    bind_dn: cn=admin,dc=example,dc=com\n    bind_pw: password\n\n#\n# The same as in the previous example but with the authentication details\n# stored in the ldap_auth variable:\n#\n# ldap_auth:\n#   server_uri: ldap://localhost/\n#   bind_dn: cn=admin,dc=example,dc=com\n#   bind_pw: password\n#\n# In the example below, 'args' is a task keyword, passed at the same level as the module\n- name: Get rid of an old entry\n  community.general.ldap_entry:\n    dn: ou=stuff,dc=example,dc=com\n    state: absent\n  args: \"{{ ldap_auth }}\"\n\"\"\"\n\n\nRETURN = \"\"\"\n# Default return values\n\"\"\"\n\nimport traceback\n\nfrom ansible.module_utils.basic import AnsibleModule, missing_required_lib\nfrom ansible.module_utils.common.text.converters import to_native, to_bytes\nfrom ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs\n\nLDAP_IMP_ERR = None\ntry:\n    import ldap.modlist\n\n    HAS_LDAP = True\nexcept ImportError:\n    LDAP_IMP_ERR = traceback.format_exc()\n    HAS_LDAP = False\n\n\nclass LdapEntry(LdapGeneric):\n    def __init__(self, module):\n        LdapGeneric.__init__(self, module)\n\n        # Shortcuts\n        self.state = self.module.params['state']\n\n        # Add the objectClass into the list of attributes\n        self.module.params['attributes']['objectClass'] = (\n            self.module.params['objectClass'])\n\n        # Load attributes\n        if self.state == 'present':\n            self.attrs = self._load_attrs()\n\n    def _load_attrs(self):\n        \"\"\" Turn attribute's value to array. \"\"\"\n        attrs = {}\n\n        for name, value in self.module.params['attributes'].items():\n            if isinstance(value, list):\n                attrs[name] = list(map(to_bytes, value))\n            else:\n                attrs[name] = [to_bytes(value)]\n\n        return attrs\n\n    def add(self):\n        \"\"\" If self.dn does not exist, returns a callable that will add it. \"\"\"\n        def _add():\n            self.connection.add_s(self.dn, modlist)\n\n        if not self._is_entry_present():\n            modlist = ldap.modlist.addModlist(self.attrs)\n            action = _add\n        else:\n            action = None\n\n        return action\n\n    def delete(self):\n        \"\"\" If self.dn exists, returns a callable that will delete it. \"\"\"\n        def _delete():\n            self.connection.delete_s(self.dn)\n\n        if self._is_entry_present():\n            action = _delete\n        else:\n            action = None\n\n        return action\n\n    def _is_entry_present(self):\n        try:\n            self.connection.search_s(self.dn, ldap.SCOPE_BASE)\n        except ldap.NO_SUCH_OBJECT:\n            is_present = False\n        else:\n            is_present = True\n\n        return is_present\n\n\ndef main():\n    module = AnsibleModule(\n        argument_spec=gen_specs(\n            attributes=dict(default={}, type='dict'),\n            objectClass=dict(type='list', elements='str'),\n            state=dict(default='present', choices=['present', 'absent']),\n        ),\n        required_if=[('state', 'present', ['objectClass'])],\n        supports_check_mode=True,\n    )\n\n    if not HAS_LDAP:\n        module.fail_json(msg=missing_required_lib('python-ldap'),\n                         exception=LDAP_IMP_ERR)\n\n    state = module.params['state']\n\n    # Instantiate the LdapEntry object\n    ldap = LdapEntry(module)\n\n    # Get the action function\n    if state == 'present':\n        action = ldap.add()\n    elif state == 'absent':\n        action = ldap.delete()\n\n    # Perform the action\n    if action is not None and not module.check_mode:\n        try:\n            action()\n        except Exception as e:\n            module.fail_json(msg=\"Entry action failed.\", details=to_native(e), exception=traceback.format_exc())\n\n    module.exit_json(changed=(action is not None))\n\n\nif __name__ == '__main__':\n    main()\n", "path": "plugins/modules/net_tools/ldap/ldap_entry.py"}], "after_files": [{"content": "#!/usr/bin/python\n# -*- coding: utf-8 -*-\n\n# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>\n# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>\n#\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import absolute_import, division, print_function\n__metaclass__ = type\n\n\nDOCUMENTATION = '''\n---\nmodule: ldap_entry\nshort_description: Add or remove LDAP entries.\ndescription:\n  - Add or remove LDAP entries. This module only asserts the existence or\n    non-existence of an LDAP entry, not its attributes. To assert the\n    attribute values of an entry, see M(community.general.ldap_attrs).\nnotes:\n  - The default authentication settings will attempt to use a SASL EXTERNAL\n    bind over a UNIX domain socket. This works well with the default Ubuntu\n    install for example, which includes a cn=peercred,cn=external,cn=auth ACL\n    rule allowing root to modify the server configuration. If you need to use\n    a simple bind to access your server, pass the credentials in I(bind_dn)\n    and I(bind_pw).\nauthor:\n  - Jiri Tyr (@jtyr)\nrequirements:\n  - python-ldap\noptions:\n  attributes:\n    description:\n      - If I(state=present), attributes necessary to create an entry. Existing\n        entries are never modified. To assert specific attribute values on an\n        existing entry, use M(community.general.ldap_attrs) module instead.\n    type: dict\n  objectClass:\n    description:\n      - If I(state=present), value or list of values to use when creating\n        the entry. It can either be a string or an actual list of\n        strings.\n    type: list\n    elements: str\n  state:\n    description:\n      - The target state of the entry.\n    choices: [present, absent]\n    default: present\n    type: str\n  recursive:\n    description:\n      - If I(state=delete), a flag indicating whether a single entry or the\n        whole branch must be deleted.\n    type: bool\n    default: false\n    version_added: 4.6.0\nextends_documentation_fragment:\n- community.general.ldap.documentation\n\n'''\n\n\nEXAMPLES = \"\"\"\n- name: Make sure we have a parent entry for users\n  community.general.ldap_entry:\n    dn: ou=users,dc=example,dc=com\n    objectClass: organizationalUnit\n\n- name: Make sure we have an admin user\n  community.general.ldap_entry:\n    dn: cn=admin,dc=example,dc=com\n    objectClass:\n      - simpleSecurityObject\n      - organizationalRole\n    attributes:\n      description: An LDAP administrator\n      userPassword: \"{SSHA}tabyipcHzhwESzRaGA7oQ/SDoBZQOGND\"\n\n- name: Get rid of an old entry\n  community.general.ldap_entry:\n    dn: ou=stuff,dc=example,dc=com\n    state: absent\n    server_uri: ldap://localhost/\n    bind_dn: cn=admin,dc=example,dc=com\n    bind_pw: password\n\n#\n# The same as in the previous example but with the authentication details\n# stored in the ldap_auth variable:\n#\n# ldap_auth:\n#   server_uri: ldap://localhost/\n#   bind_dn: cn=admin,dc=example,dc=com\n#   bind_pw: password\n#\n# In the example below, 'args' is a task keyword, passed at the same level as the module\n- name: Get rid of an old entry\n  community.general.ldap_entry:\n    dn: ou=stuff,dc=example,dc=com\n    state: absent\n  args: \"{{ ldap_auth }}\"\n\"\"\"\n\n\nRETURN = \"\"\"\n# Default return values\n\"\"\"\n\nimport traceback\n\nfrom ansible.module_utils.basic import AnsibleModule, missing_required_lib\nfrom ansible.module_utils.common.text.converters import to_native, to_bytes\nfrom ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs\n\nLDAP_IMP_ERR = None\ntry:\n    import ldap.modlist\n    import ldap.controls\n\n    HAS_LDAP = True\nexcept ImportError:\n    LDAP_IMP_ERR = traceback.format_exc()\n    HAS_LDAP = False\n\n\nclass LdapEntry(LdapGeneric):\n    def __init__(self, module):\n        LdapGeneric.__init__(self, module)\n\n        # Shortcuts\n        self.state = self.module.params['state']\n        self.recursive = self.module.params['recursive']\n\n        # Add the objectClass into the list of attributes\n        self.module.params['attributes']['objectClass'] = (\n            self.module.params['objectClass'])\n\n        # Load attributes\n        if self.state == 'present':\n            self.attrs = self._load_attrs()\n\n    def _load_attrs(self):\n        \"\"\" Turn attribute's value to array. \"\"\"\n        attrs = {}\n\n        for name, value in self.module.params['attributes'].items():\n            if isinstance(value, list):\n                attrs[name] = list(map(to_bytes, value))\n            else:\n                attrs[name] = [to_bytes(value)]\n\n        return attrs\n\n    def add(self):\n        \"\"\" If self.dn does not exist, returns a callable that will add it. \"\"\"\n        def _add():\n            self.connection.add_s(self.dn, modlist)\n\n        if not self._is_entry_present():\n            modlist = ldap.modlist.addModlist(self.attrs)\n            action = _add\n        else:\n            action = None\n\n        return action\n\n    def delete(self):\n        \"\"\" If self.dn exists, returns a callable that will delete either\n        the item itself if the recursive option is not set or the whole branch\n        if it is. \"\"\"\n        def _delete():\n            self.connection.delete_s(self.dn)\n\n        def _delete_recursive():\n            \"\"\" Attempt recurive deletion using the subtree-delete control.\n            If that fails, do it manually. \"\"\"\n            try:\n                subtree_delete = ldap.controls.ValueLessRequestControl('1.2.840.113556.1.4.805')\n                self.connection.delete_ext_s(self.dn, serverctrls=[subtree_delete])\n            except ldap.NOT_ALLOWED_ON_NONLEAF:\n                search = self.connection.search_s(self.dn, ldap.SCOPE_SUBTREE, attrlist=('dn',))\n                search.reverse()\n                for entry in search:\n                    self.connection.delete_s(entry[0])\n\n        if self._is_entry_present():\n            if self.recursive:\n                action = _delete_recursive\n            else:\n                action = _delete\n        else:\n            action = None\n\n        return action\n\n    def _is_entry_present(self):\n        try:\n            self.connection.search_s(self.dn, ldap.SCOPE_BASE)\n        except ldap.NO_SUCH_OBJECT:\n            is_present = False\n        else:\n            is_present = True\n\n        return is_present\n\n\ndef main():\n    module = AnsibleModule(\n        argument_spec=gen_specs(\n            attributes=dict(default={}, type='dict'),\n            objectClass=dict(type='list', elements='str'),\n            state=dict(default='present', choices=['present', 'absent']),\n            recursive=dict(default=False, type='bool'),\n        ),\n        required_if=[('state', 'present', ['objectClass'])],\n        supports_check_mode=True,\n    )\n\n    if not HAS_LDAP:\n        module.fail_json(msg=missing_required_lib('python-ldap'),\n                         exception=LDAP_IMP_ERR)\n\n    state = module.params['state']\n\n    # Instantiate the LdapEntry object\n    ldap = LdapEntry(module)\n\n    # Get the action function\n    if state == 'present':\n        action = ldap.add()\n    elif state == 'absent':\n        action = ldap.delete()\n\n    # Perform the action\n    if action is not None and not module.check_mode:\n        try:\n            action()\n        except Exception as e:\n            module.fail_json(msg=\"Entry action failed.\", details=to_native(e), exception=traceback.format_exc())\n\n    module.exit_json(changed=(action is not None))\n\n\nif __name__ == '__main__':\n    main()\n", "path": "plugins/modules/net_tools/ldap/ldap_entry.py"}]}