├── README.md ├── example_ldap_inventory.yaml ├── plugins └── inventory │ └── ldap_inventory.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Ansible LDAP Inventory Plugin 2 | 3 | This plugin was designed to query active directory and get a list of machines to use as an inventory. 4 | Groups are auto generated off of OU structure and optionally group membership. 5 | So for example `cn=computer1,ou=servers,ou=windows,dc=mycompany,dc=local` would create the following inventory : 6 | ``` 7 | "all": { 8 | "children": [ 9 | "windows" 10 | ] 11 | }, 12 | "windows": { 13 | "children": [ 14 | "windows_servers" 15 | ] 16 | }, 17 | "windows_servers": { 18 | "hosts": [ 19 | "computer1" 20 | ] 21 | } 22 | ``` 23 | 24 | ## Prerequisites 25 | 26 | The ldap inventory works with python2 and python3. 27 | 28 | **The following package is required :** 29 | * [python-ldap](https://www.python-ldap.org/en/latest/) 30 | 31 | It can be installed in one of the following ways : 32 | 33 | *pip install -r requirements.txt* 34 | 35 | or 36 | 37 | *pip install python-ldap* 38 | 39 | ## Configuration Example 40 | Place the file `ldap_inventory.py` into your base folder under `.\plugins\inventory\` 41 | 42 | Create a file that ends with `ldap_inventory.yaml` in your base directory. 43 | It is recommended you vault the entire file if storing passwords in plaintext(until ansible supports vaulted strings in config files) `ansible-vault edit ldap_inventory.yaml` 44 | 45 | >`LDAP_USER`, `LDAP_PASS` and `SEARCH_OU` environmental variables can be used instead of including them in the configuration file. This is helpful if using the plugin in [Ansible Tower/AWX](https://github.com/ansible/awx). 46 | 47 | Example `ldap_inventory.yaml` : 48 | ```yaml 49 | --- 50 | plugin: ldap_inventory 51 | domain: 'ldaps://adserver.domain.local:636' 52 | username: user@domain.local 53 | password: "password" 54 | search_ou: "OU=Servers,OU=Windows,DC=domain,DC=local" 55 | ``` 56 | 57 | ## Parameters 58 | ### `account_age` 59 | > LDAP attribute filter for the lastLogonTimestamp field. This value is generally updated every 14 days. Timestamps older indicate inactive computer accounts. Setting to 0 disables check. Value is in days. 60 | 61 | * default: `0` 62 | 63 | ### `auth_type` 64 | > Defines the type of authentication used when connecting to Active Directory (LDAP). When using `simple`, the **`username`** and **`password`** parameters must be set. When using `gssapi`, run **kinit** before running Ansible to get a valid Kerberos ticket. 65 | 66 | * allowed values: `simple`, `gssapi` 67 | * default: `simple` 68 | 69 | ### `domain` 70 | > The domain to search in to retrieve inventory. This could either be a Windows domain name visible to the Ansible controller from DNS or a specific domain controller FQDN. Supports either just the domain/host name or an explicit LDAP URI with the domain/host already filled in. If the URI is set, **`port`** and **`scheme`** are ignored. 71 | * required: true 72 | 73 | **examples:** 74 | ```yaml 75 | domain: "local.com" 76 | ``` 77 | ```yaml 78 | domain: "dc1.local.com" 79 | ``` 80 | ```yaml 81 | domain: "ldaps://dc1.local.com:636" 82 | ``` 83 | ```yaml 84 | domain: "ldap://dc1.local.com" 85 | ``` 86 | ### `group_membership` 87 | >Enables parsing the ldap groups that the computer account is a memberOf. Groups are returned lower case. 88 | * default: `"False"` 89 | 90 | **example:** 91 | ```yaml 92 | group_membership: True 93 | ``` 94 | 95 | ### `group_membership_filter` 96 | >When we query for Group membership of the computer object, this allows you to only include names that match the pattern provided. 97 | * default: `""` 98 | 99 | **example:** 100 | ```yaml 101 | group_membership: "security-*" 102 | ``` 103 | 104 | ### `exclude_groups` 105 | >Exclude a list of groups from being included in the inventory. This will match substrings. 106 | * default: `""` 107 | 108 | **example:** 109 | ```yaml 110 | exclude_groups: "windows_group1,windows_group2" 111 | ``` 112 | 113 | ### `exclude_hosts` 114 | >Exclude a list of hosts from being included in the inventory. This will match substrings. 115 | * default: `""` 116 | 117 | **example:** 118 | ```yaml 119 | exclude_hosts: "hostname1,hostname2" 120 | ``` 121 | 122 | ### `extra_groups` 123 | >Add a list of groups to the inventory under the top-level `all` group and place 124 | >all hosts into these groups. This is useful in an AWX/Tower scenario where 125 | >hosts need to be put into a named group to pick up variable values specific to 126 | >that. AWX/Tower performs this variable assignment at inventory sync time and 127 | >not playbook execution time. 128 | * default: [] 129 | 130 | **example:** 131 | ```yaml 132 | extra_groups: 133 | - foo 134 | - bar 135 | - baz 136 | ``` 137 | 138 | ### `fqdn_format` 139 | >Specifies if we should use FQDN instead of shortname for hosts. 140 | * Allow Values: `True`, `False` 141 | * Default: `False` 142 | 143 | ### `ldap_filter` 144 | >LDAP filter used to find objects. You should not usually need to change this. 145 | * Allowed Values: [RFC 4515](https://tools.ietf.org/html/rfc4515.html) 146 | * Default: `"(objectClass=Computer)"` 147 | 148 | ### `online_only` 149 | >Performs a ping check of the machine before adding to inventory. Note: Does not work under bubblewrap (Tower/AWX) due to setuid flag of ping. 150 | * Allow Values: `True`, `False` 151 | * Default: `False` 152 | 153 | ### `password` 154 | >Password used to authenticate LDAP user when **`auth_type`** is set to `simple`. Can use environmental variable `LDAP_PASSWORD` instead of setting in config. 155 | * required: true 156 | 157 | **example:** 158 | ```yaml 159 | password: "Password123!" 160 | ``` 161 | 162 | ### `port` 163 | >Port used to connect to Domain Controller. If **`domain`** URI contains ldap or ldaps this is ignored. 164 | * Default: `389` for ldap, `636` for ldaps 165 | 166 | ### `scheme` 167 | >The ldap scheme to use. When using `ldap`, it is recommended to set `auth=gssapi`, or `start_tls=yes`, otherwise traffic will be in plaintext. This parameter is not required and can be determined from the **`domain`** URI or **`port`**. 168 | * Allowed Values: `ldap`, `ldaps` 169 | * Default: `ldap` 170 | 171 | ### `search_ou` 172 | >LDAP path to search for computer objects. Can use environmental variable `SEARCH_OU` instead of setting in config. 173 | * required: true 174 | 175 | **example:** 176 | ```yaml 177 | search_ou: "CN=Computers,DC=local,DC=com" 178 | ``` 179 | 180 | ### `username` 181 | >LDAP user account used to bind LDAP search when **`auth_type`** is set to `simple`. Can use environmental variable `LDAP_USER` instead of setting in config. 182 | * required: true 183 | 184 | **examples:** 185 | ```yaml 186 | username: "username@local.com" 187 | ``` 188 | ```yaml 189 | username: "domain\\\\username" 190 | ``` 191 | 192 | ### `validate_certs` 193 | >Controls if verfication is done of SSL certificates for secure (ldaps://) connections. 194 | * Allow Values: `True`, `False` 195 | * Default: `True` 196 | 197 | 198 | 199 | 200 | ## Testing 201 | 202 | `ansible-inventory -i ldap_inventory --list` 203 | 204 | `ansible-inventory -i ldap_inventory --list --vault-id=@prompt` (when vaulted) 205 | 206 | ** Running a playbook ** 207 | 208 | `ansible-playbook -i ldap_inventory.yaml adhoc.yaml --vault-id@prompt ` 209 | -------------------------------------------------------------------------------- /example_ldap_inventory.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: ldap_inventory 3 | domain: 'ldaps://adserver.domain.local:636' 4 | auth_type: simple 5 | username: user@domain.local 6 | password: "password" 7 | search_ou: "OU=Servers,OU=Windows,DC=domain,DC=local" 8 | account_age: 15 9 | -------------------------------------------------------------------------------- /plugins/inventory/ldap_inventory.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | DOCUMENTATION = ''' 7 | name: ldap_inventory 8 | author: Joshua Robinett (@jshinryz) 9 | plugin_type: inventory 10 | short_description: LDAP Inventory Source 11 | description: 12 | - Recursively get inventory from LDAP organizational unit. Creates both hosts and groups from LDAP 13 | - Setup by creating a YAML config file , it's name must end with ldap_inventory.yml or ldap_inventory.yaml. 14 | - The inventory_hostname is pulled from the 'Name' LDAP attribute. 15 | options: 16 | plugin: 17 | description: "Token that ensures this is a source file for the 'ldap_inventory' plugin" 18 | required: True 19 | choices: ['ldap_inventory'] 20 | online_only: 21 | description: 22 | - "Enables checking of hosts using ICMP ping before adding to inventory" 23 | - "Note: This may not be compatabile with bubblewrap , which is enabled by default in Ansible Tower" 24 | default: False 25 | type: boolean 26 | required: False 27 | group_membership: 28 | description: 29 | - "Enables parsing the ldap groups that the computer account is a memberOf" 30 | - "Groups are returned lower case." 31 | default: False 32 | type: boolean 33 | required: False 34 | group_membership_filter: 35 | description: 36 | - When we query for Group membership of the computer object, this allows you to only include names that match the pattern provided. 37 | - For example, if you only wanted groups that start with security-* 38 | - " group_membership_filter: security-*" 39 | required: False 40 | default: "*" 41 | type: str 42 | account_age: 43 | description: 44 | - "LDAP attribute filter for the lastLogonTimestamp field. This value is generally updated every 14 days." 45 | - "Timestamps older indicate inactive computer accounts. Setting to 0 disables check. Value is in days" 46 | default: 0 47 | required: False 48 | domain: 49 | description: 50 | - The domain to search in to retrieve the LAPS password. 51 | - This could either be a Windows domain name visible to the Ansible controller from DNS or a specific domain controller FQDN. 52 | - Supports either just the domain/host name or an explicit LDAP URI with the domain/host already filled in. 53 | - If the URI is set, I(port) and I(scheme) are ignored. 54 | - "Examples: " 55 | - " local.com" 56 | - " dc1.local.com" 57 | - " ldaps://dc1.local.com:636" 58 | - " ldap://dc1.local.com" 59 | required: True 60 | type: str 61 | port: 62 | description: 63 | - Port used to connect to Domain Controller (389 for ldap, 636 for ldaps) 64 | - If I(kdc) is already an LDAP URI then this is ignored. 65 | required: False 66 | type: int 67 | scheme: 68 | description: 69 | - The LDAP scheme to use. 70 | - When using C(ldap), it is recommended to set C(auth=gssapi), or C(start_tls=yes), otherwise traffic will be in plaintext. 71 | - If I(kdc) is already an LDAP URI then this is ignored. 72 | choices: 73 | - ldap 74 | - ldaps 75 | default: ldap 76 | type: str 77 | required: False 78 | search_ou: 79 | description: 80 | - "LDAP path to search for computer objects." 81 | - "Example: CN=Computers,DC=local,DC=com" 82 | env: 83 | - name: SEARCH_OU 84 | required: True 85 | username: 86 | description: 87 | - "LDAP user account used to bind our LDAP search when auth_type is set to simple" 88 | - "Examples:" 89 | - " username@local.com" 90 | - " domain\\\\username" 91 | env: 92 | - name: LDAP_USER 93 | required: False 94 | password: 95 | description: 96 | - "LDAP user password used to bind our LDAP search." 97 | - "Example: Password123!" 98 | env: 99 | - name: LDAP_PASS 100 | required: False 101 | ldap_filter: 102 | description: 103 | - "Filter used to find computer objects." 104 | - "Example: (objectClass=computer)" 105 | required: False 106 | default: "(objectClass=Computer)" 107 | exclude_groups: 108 | description: 109 | - "List of groups to not include." 110 | - "Example: " 111 | - " exclude_groups: " 112 | - " - group1" 113 | - " - group2" 114 | type: list 115 | required: False 116 | default: [] 117 | exclude_hosts: 118 | description: 119 | - "List of computers to not include." 120 | - "Example: " 121 | - " exclude_hosts: " 122 | - " - host01" 123 | - " - host02" 124 | type: list 125 | required: False 126 | default: [] 127 | validate_certs: 128 | description: "Controls if verfication is done of SSL certificates for secure (ldaps://) connections." 129 | default: True 130 | required: False 131 | fqdn_format: 132 | description: "Controls if the hostname is returned to the inventory as FQDN or shortname" 133 | default: False 134 | required: False 135 | type: bool 136 | auth_type: 137 | description: 138 | - Defines the type of authentication used when connecting to Active Directory (LDAP). 139 | - When using C(simple), the I(username) and (password) options must be set. This requires support of LDAPS (SSL) 140 | - When using C(gssapi), run C(kinit) before running Ansible to get a valid Kerberos ticket. 141 | choices: 142 | - simple 143 | - gssapi 144 | type: str 145 | extra_groups: 146 | description: "A list of additional groups to add under 'all' and contain all discovered hosts." 147 | default: [] 148 | required: False 149 | type: list 150 | ''' 151 | 152 | EXAMPLES = ''' 153 | # Sample configuration file for LDAP dynamic inventory 154 | plugin: ldap_inventory 155 | domain: ldaps://ldapserver.local.com:636 156 | search_ou: CN=Computers,DC=local,DC=com 157 | auth_type: simple 158 | username: username@local.com 159 | password: Password123! 160 | ''' 161 | 162 | import os 163 | import re 164 | import traceback 165 | import subprocess 166 | import multiprocessing 167 | from datetime import datetime, timedelta 168 | from ansible.plugins.inventory import BaseInventoryPlugin, Constructable 169 | from ansible.utils.display import Display 170 | from ansible.errors import AnsibleError 171 | from ansible.module_utils._text import to_native 172 | from ansible.module_utils.basic import missing_required_lib 173 | 174 | LDAP_IMP_ERR = None 175 | try : 176 | import ldap 177 | import ldapurl 178 | HAS_LDAP = True 179 | except ImportError: 180 | HAS_LDAP = False 181 | LDAP_IMP_ERR = traceback.format_exc() 182 | 183 | hostname_field = "name" 184 | 185 | display = Display() 186 | 187 | try: 188 | cpus = multiprocessing.cpu_count() 189 | except NotImplementedError: 190 | cpus = 4 #Arbitrary Default 191 | 192 | if not HAS_LDAP: 193 | msg = missing_required_lib("python-ldap", url="https://pypi.org/project/python-ldap/") 194 | msg += ". Import Error: %s" % LDAP_IMP_ERR 195 | raise AnsibleError(msg) 196 | 197 | class PagedResultsSearchObject: 198 | page_size = 50 199 | 200 | def paged_search_ext_s(self,base,scope,filterstr='(objectClass=Computer)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): 201 | """ 202 | Behaves exactly like LDAPObject.search_ext_s() but internally uses the 203 | simple paged results control to retrieve search results in chunks. 204 | 205 | This is non-sense for really large results sets which you would like 206 | to process one-by-one 207 | """ 208 | req_ctrl = ldap.controls.SimplePagedResultsControl(True,size=self.page_size,cookie='') 209 | 210 | # Send first search request 211 | msgid = self.search_ext( 212 | base, 213 | ldap.SCOPE_SUBTREE, 214 | filterstr, 215 | attrlist, 216 | serverctrls=(serverctrls or [])+[req_ctrl] 217 | ) 218 | 219 | result_pages = 0 220 | all_results = [] 221 | 222 | while True: 223 | rtype, rdata, rmsgid, rctrls = self.result3(msgid) 224 | all_results.extend(rdata) 225 | result_pages += 1 226 | # Extract the simple paged results response control 227 | pctrls = [ 228 | c 229 | for c in rctrls 230 | if c.controlType == ldap.controls.SimplePagedResultsControl.controlType 231 | ] 232 | if pctrls: 233 | if pctrls[0].cookie: 234 | # Copy cookie from response control to request control 235 | req_ctrl.cookie = pctrls[0].cookie 236 | msgid = self.search_ext( 237 | base, 238 | ldap.SCOPE_SUBTREE, 239 | filterstr, 240 | attrlist, 241 | serverctrls=(serverctrls or [])+[req_ctrl] 242 | ) 243 | else: 244 | break 245 | return result_pages,all_results 246 | 247 | 248 | class MyLDAPObject(ldap.ldapobject.LDAPObject,PagedResultsSearchObject): 249 | pass 250 | 251 | 252 | 253 | 254 | def check_online(hostObject): 255 | try: 256 | hostname = hostObject[1][hostname_field][0].decode('utf-8') 257 | except: 258 | returnObject = hostObject + ({'online':False},) 259 | return returnObject 260 | result = subprocess.Popen(["ping -c 1 " + hostname + ' >/dev/null 2>&1; echo $?'],shell=True,stdout=subprocess.PIPE) 261 | out,err = result.communicate() 262 | out = out.decode('utf-8').replace("\n","") 263 | try : 264 | err = err.decode('utf-8').replace("\n","") 265 | except: 266 | err = "" 267 | if(out == "0"): 268 | returnObject = hostObject + ({'online':True},) 269 | return returnObject 270 | else: 271 | returnObject = hostObject + ({'online':False},) 272 | return returnObject 273 | 274 | class InventoryModule(BaseInventoryPlugin, Constructable): 275 | 276 | NAME = 'ldap_inventory' 277 | 278 | def _set_config(self): 279 | """ 280 | Set config options 281 | """ 282 | self.domain = self.get_option('domain') 283 | self.port = self.get_option('port') 284 | self.username = self.get_option('username') 285 | self.password = self.get_option('password') 286 | self.search_ou = self.get_option('search_ou') 287 | self.group_membership = self.get_option('group_membership') 288 | self.account_age = self.get_option('account_age') 289 | self.validate_certs = self.get_option('validate_certs') 290 | self.online_only = self.get_option('online_only') 291 | self.exclude_groups = self.get_option('exclude_groups') 292 | self.exclude_hosts = self.get_option('exclude_hosts') 293 | self.use_fqdn = self.get_option('fqdn_format') 294 | self.auth_type = self.get_option('auth_type') 295 | self.scheme = self.get_option('scheme') 296 | self.ldap_filter = self.get_option('ldap_filter') 297 | self.group_membership_filter = self.get_option('group_membership_filter') 298 | self.extra_groups = self.get_option('extra_groups') 299 | 300 | 301 | def _ldap_bind(self): 302 | """ 303 | Set LDAP binding 304 | """ 305 | 306 | #ldap.set_option(ldap.OPT_DEBUG_LEVEL, 4095) 307 | 308 | if self.auth_type == 'gssapi' : 309 | if not ldap.SASL_AVAIL: 310 | raise AnsibleLookupError("Cannot use auth=gssapi when SASL is not configured with the local LDAP install") 311 | if self.username or self.password: 312 | raise AnsibleError("Explicit credentials are not supported when auth_type='gssapi'. Call kinit outside of Ansible") 313 | elif self.auth_type == 'simple' and not (self.username and self.password): 314 | raise AnsibleError("The username and password values are required when auth_type=simple") 315 | else: 316 | if self.username and self.password: 317 | self.auth_type = 'simple' 318 | elif ldap.SASL_AVAIL: 319 | self.auth_type == 'gssapi' 320 | else: 321 | raise AnsibleError("Invalid auth_type value '%s': expecting either 'gssapi', or 'simple'" % self.auth_type) 322 | 323 | if ldapurl.isLDAPUrl(self.domain): 324 | ldap_url = ldapurl.LDAPUrl(ldapUrl=self.domain) 325 | else: 326 | self.port = self.port if self.port else 389 if self.scheme == 'ldap' else 636 327 | ldap_url = ldapurl.LDAPUrl(hostport="%s:%d" % (self.domain, self.port), urlscheme=self.scheme) 328 | 329 | if self.validate_certs is False : 330 | ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) 331 | 332 | if not ldap.TLS_AVAIL and ldap_url.urlscheme == 'ldaps': 333 | raise AnsibleLookupError("Cannot use TLS as the local LDAP installed has not been configured to support it") 334 | 335 | conn_url = ldap_url.initializeUrl() 336 | #self.ldap_session = MyLDAPObject(conn_url, trace_level=3) # higher trace levels 337 | self.ldap_session = MyLDAPObject(conn_url) 338 | self.ldap_session.page_size = 900 339 | self.ldap_session.set_option(ldap.OPT_PROTOCOL_VERSION, 3) 340 | self.ldap_session.set_option(ldap.OPT_REFERRALS, 0) 341 | 342 | if self.auth_type == 'simple': 343 | try: 344 | self.ldap_session.bind_s(self.username, self.password, ldap.AUTH_SIMPLE) 345 | except ldap.LDAPError as err: 346 | raise AnsibleError("Failed to simple bind against LDAP host '%s': %s " % (conn_url, to_native(err))) 347 | else: 348 | # Windows AD does not allow seal/sign when over TLS 349 | if ldap_url.urlscheme == 'ldaps': 350 | self.ldap_session.set_option(ldap.OPT_X_SASL_SSF_MAX, 0) 351 | 352 | try: 353 | self.ldap_session.sasl_gssapi_bind_s() 354 | except ldap.AUTH_UNKNOWN as err: 355 | # The SASL GSSAPI binding is not installed, e.g. cyrus-sasl-gssapi. Give a better error message than what python-ldap provides 356 | raise AnsibleError("Failed to do a sasl bind against LDAP host '%s', the GSSAPI mech is not installed: %s" % (conn_url, to_native(err))) 357 | except ldap.LDAPError as err: 358 | raise AnsibleError("Failed to do a sasl bind against LDAP host '%s': %s" % (conn_url, to_native(err))) 359 | 360 | 361 | 362 | 363 | 364 | def _detect_group(self, ouString): 365 | """ 366 | Detect groups in OU string 367 | """ 368 | groups = [] 369 | foundOUs = re.findall('(?u)OU=([^,]+)',ouString) 370 | foundOUs = [x.lower() for x in foundOUs] 371 | foundOUs = [x.replace("-","_") for x in foundOUs] 372 | foundOUs = [x.replace(" ","_") for x in foundOUs] 373 | foundOUs = list(reversed(foundOUs)) 374 | for i in range(len(foundOUs)): 375 | group = '_'.join(elem for elem in foundOUs[0:i+1]) 376 | groups.append(group) 377 | return groups 378 | 379 | def verify_file(self, path): 380 | ''' 381 | :param loader: an ansible.parsing.dataloader.DataLoader object 382 | :param path: the path to the inventory config file 383 | :return the contents of the config file 384 | ''' 385 | if super(InventoryModule, self).verify_file(path): 386 | if path.endswith(('ldap_inventory.yml', 'ldap_inventory.yaml')): 387 | return True 388 | display.debug("DEBUG: ldap inventory filename must end with 'ldap_inventory.yml' or 'ldap_inventory.yaml'") 389 | return False 390 | 391 | def parse(self, inventory, loader, path, cache=False): 392 | """ 393 | Parses the inventory file 394 | """ 395 | super(InventoryModule, self).parse(inventory, loader, path) 396 | 397 | self._read_config_data(path) 398 | self._set_config() 399 | 400 | if not self.search_ou: 401 | raise AnsibleError("Search base not set in search_ou config option or SEARCH_OU environmental variable") 402 | 403 | ldap_search_scope = ldap.SCOPE_SUBTREE 404 | if not self.ldap_filter: 405 | ldap_type_groupFilter = '(objectClass=Computer)' 406 | else: 407 | ldap_type_groupFilter = self.ldap_filter # Todo check if query is valid 408 | 409 | if self.account_age > 0: 410 | ldap_search_attributeFilter = [hostname_field,'lastLogontimeStamp'] 411 | else: 412 | ldap_search_attributeFilter = [hostname_field] 413 | 414 | timestamp_daysago = datetime.today() - timedelta(days=self.account_age) 415 | timestamp_filter_epoch = timestamp_daysago.strftime("%s") 416 | windows_tick = 10000000 417 | windows_to_epoc_sec = 11644473600 418 | timestamp_filter_windows = ( int(timestamp_filter_epoch) + windows_to_epoc_sec ) * windows_tick 419 | 420 | 421 | 422 | # Call LDAP query 423 | self._ldap_bind() 424 | 425 | try: 426 | pages, ldap_results = self.ldap_session.paged_search_ext_s(base=self.search_ou, scope=ldap_search_scope, filterstr=ldap_type_groupFilter, attrlist=ldap_search_attributeFilter) 427 | 428 | except ldap.LDAPError as err: 429 | raise AnsibleError("Unable to perform query against LDAP server '%s' reason: %s" % (self.domain, to_native(err))) 430 | ldap_results = [] 431 | display.debug('DEBUG: ldap_results Received %d results in %d pages.' % (len(ldap_results),pages) ) 432 | 433 | #Parse the results. 434 | if self.online_only : 435 | pool = multiprocessing.Pool(processes=cpus) 436 | parsedResult = pool.map(check_online, ldap_results) 437 | else: 438 | parsedResult = ldap_results 439 | 440 | for item in parsedResult: 441 | if isinstance(item[1],dict) is False or len(item[1]) != len(ldap_search_attributeFilter) : 442 | display.debug("DEBUG: Skipping an possible corrupt object " + str(item[1]) + " " + str(item[0])) 443 | continue 444 | if self.online_only and item[2]['online'] is False : 445 | continue 446 | hostName = item[1][hostname_field][0].decode("utf-8").lower() 447 | display.debug("DEBUG: " + hostName + " processing host") 448 | pattern = re.compile('^DC') 449 | root_split = item[0].split(',') 450 | root_match = [s for s in root_split if pattern.match(s) ] 451 | root_ou = ','.join(root_match) 452 | #root_ou = "OU=Groups,%s" % root_ou 453 | ldapGroups = [] 454 | 455 | if self.use_fqdn is True : 456 | domainName = "." + item[0].split('DC=',1)[1].replace(',DC=','.') 457 | hostName = hostName + domainName.lower() 458 | 459 | if self.account_age > 0: 460 | item_time = int(item[1]['lastLogonTimestamp'][0]) 461 | 462 | 463 | 464 | #Check for hostname filter 465 | if any(sub in hostName for sub in self.exclude_hosts) : 466 | display.debug("DEBUG: Skipping " + hostName + " as it was found in exclude_hosts") 467 | continue 468 | #Check age of lastLogontime vs supplied expiration window. 469 | if self.account_age > 0 and timestamp_filter_windows > item_time and item_time > 0: 470 | display.debug("DEBUG: [" + hostName + "] appears to be expired. lastLogontime: " + str(item_time) + " comparison timestamp: " + str(timestamp_filter_windows)) 471 | continue 472 | 473 | ouGroups = self._detect_group(item[0]) 474 | 475 | 476 | if self.group_membership: 477 | groupFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:=%s)(name=%s))" % (item[0], self.group_membership_filter) 478 | try: 479 | ldapSearch = self.ldap_session.search_ext_s(base=root_ou, scope=ldap_search_scope, filterstr=groupFilter, attrlist=["cn"]) 480 | except ldap.LDAPError as err: 481 | raise AnsibleError("Unable to perform query against LDAP server '%s' reason: %s" % (self.domain, to_native(err))) 482 | if len(ldapSearch) > 0 : 483 | for g in ldapSearch : 484 | if re.search("^cn", str(g[0]).lower()): 485 | groupName = g[0].replace("-","_").replace(" ","_").lower().split(",",1)[0][3:] 486 | ldapGroups.append(groupName) 487 | #Debug the search settings used to find groups. 488 | display.debug("DEBUG: ldap search for groups using settings - base=%s, scope=%s, filterstr=%s" % (root_ou,ldap_search_scope,groupFilter) ) 489 | 490 | #Check for groupname filter 491 | display.debug("DEBUG: Primary group for %s detected as %s" % (hostName, ouGroups[-1])) 492 | 493 | if any(sub in ouGroups[-1] for sub in self.exclude_groups) : 494 | display.debug("DEBUG: Skipping %s as group %s was found in ldap_exclude_groups" % (hostName, sub)) 495 | continue 496 | 497 | if any(sub in ldapGroups for sub in self.exclude_groups) : 498 | display.debug("DEBUG: Skipping %s as group %s was found in ldap_exclude_groups" % (hostName, sub)) 499 | continue 500 | 501 | 502 | if (len(ouGroups) < 1) and (len(ldapGroups) < 1): 503 | display.debug('DEBUG: No Groups were detected for %s' % hostName) 504 | continue 505 | 506 | 507 | self.inventory.add_host(hostName) 508 | 509 | for i in range(len(ouGroups)): 510 | if i > 0 : 511 | self.inventory.add_group(ouGroups[i]) 512 | self.inventory.add_child(ouGroups[i-1], ouGroups[i]) 513 | else: 514 | self.inventory.add_group(ouGroups[i]) 515 | self.inventory.add_child('all', ouGroups[i]) 516 | if ouGroups[i] == ouGroups[-1]: 517 | self.inventory.add_child(ouGroups[i], hostName) 518 | 519 | for group in ldapGroups: 520 | self.inventory.add_group(group) 521 | self.inventory.add_child(group, hostName) 522 | 523 | for eg in self.extra_groups: 524 | self.inventory.add_group(eg) 525 | self.inventory.add_child('all', eg) 526 | self.inventory.add_child(eg, hostName) 527 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-ldap --------------------------------------------------------------------------------