#!/usr/bin/env python
"""inventory_ad
===============
This script is an Ansible Dynamic Inventory script. It takes the host name of a
client, searches for it in MS Active Directory and returns it's groups. The
groups are filtered by prefix and Active Directory Organizational Unit.
This script returns the group and hostname in an Ansible compatible JSON
format. The script writes an offline cache file for later use if the Active
Directory query succeeds. This cache is returned if the Active Directory query
fails.
It always returns the common group for common tasks. It also returns the group
common-ad-bound if the result comes from an online query rather than from the
cache.
"""
__author__ = "Jan Welker"
__email__ = "jan.welker@unibas.ch"
__copyright__ = "Copyright 2017, University of Basel"
__credits__ = ["Balz Aschwanden", "Jan Welker"]
__license__ = "GPL"
from ssl import CERT_REQUIRED
from os import path
from socket import gethostname
from json import dumps
from ldap3 import Server, Connection, Tls, NTLM
from antslib import configer
[docs]def connect_to_ad(ldap_user, ldap_pw, ldap_host):
"""Connect to Active Directory and return the connection or None."""
tls = Tls(validate=CERT_REQUIRED)
server = Server(ldap_host, use_ssl=True, tls=tls)
connection = Connection(server, user=ldap_user, password=ldap_pw,
authentication=NTLM)
connection.bind()
result = connection.result['description']
if result == 'success':
return connection
else:
return None
[docs]def get_simple_host_name(fqdn):
"""Convert FQDN to simple host name and return it."""
simple_hostname = fqdn.split('.')[0]
return simple_hostname
[docs]def host_exist_in_ad(connection, simple_hostname, ldap_ou):
"""Check if host can be found in Active Directory. The host does not have
to be bound to AD it just has to exist."""
connection.search(ldap_ou, '(cn=%s)' % simple_hostname,
attributes=['cn'])
try:
response = connection.response[0]['attributes']['cn']
except KeyError:
response = ''
return bool(response)
[docs]def get_computer_dn(connection, simple_hostname, ldap_ou):
"""Take the simple host name and return it's distinguished name"""
connection.search(ldap_ou, '(cn=%s)' % simple_hostname,
attributes=['distinguishedName'])
try:
response = connection.response[0]['attributes']['distinguishedName']
except KeyError:
response = ''
return response
[docs]def get_computer_groups(connection, search_base, computer_dn, group_prefix):
"""Receive groups that the computer object is a member off.
member:1.2.840.113556.1.4.1941:=%s is a special Active Directory OID that
returns nested groups and not just the first level. The result is filtered
by group prefix and Organizational Unit """
result = list()
connection.search(search_base,
'(member:1.2.840.113556.1.4.1941:=%s)' % computer_dn,
attributes=['cn'])
response = connection.response
if response:
# Removing LDAP string from last line
response.pop()
# Extracting groups from ldap response
for group in response:
group_name = group['attributes']['cn']
# Only adding groups that start with the prefix
if group_prefix in group_name:
result.append(group_name)
return result
[docs]def write_cache(cache_file, output):
"""Write inventory cache to file."""
with open(cache_file, 'w') as cache:
for line in format_output(output):
cache.write(line)
[docs]def read_cache(cache_file):
"""Read cache file and return content."""
if not path.isfile(cache_file):
return False
with open(cache_file, 'r') as cache:
return cache.read()
[docs]def main():
"""Fetching groups from AD and printing them in JSON."""
cfg = configer.read_config('ad')
ants_path = path.dirname(path.realpath(__file__))
cache_file = path.join(ants_path, 'etc', cfg['cache_file'])
# Reading fully qualified host name and converting it to lower case
fqdn = gethostname().lower()
simple_host_name = get_simple_host_name(fqdn)
# Connecting to Active Directory and check connection status
ad_connection = connect_to_ad(
cfg['ldap_user'], cfg['ldap_pw'], cfg['ldap_host'])
online = bool(ad_connection)
if online:
# Initializing output
output = dict()
output[cfg['common_group']] = [fqdn]
# Checking if host is in AD
if host_exist_in_ad(ad_connection, simple_host_name,
cfg['ldap_ou_computers']):
# Looking up computers distinguished name
computer_dn = get_computer_dn(
ad_connection, simple_host_name, cfg['ldap_ou_computers'])
# Looking up computers groups
computer_groups = get_computer_groups(ad_connection,
cfg['ldap_ou_groups'],
computer_dn,
cfg['group_prefix'])
# Adding groups to output
for group in computer_groups:
output[group] = [fqdn]
# Writing output to cache file
write_cache(cache_file, output)
# Adding online Group after cache is written.
# We do not want to cache this group
output[cfg['common_ad_bound_group']] = [fqdn]
# Printing output in Ansible JSON syntax
print format_output(output)
# We are not bound to AD we are offline
else:
# Reading cache file
cached_output = read_cache(cache_file)
if cached_output:
# Printing cached
print cached_output
else:
# Printing default group
output = dict()
output['common'] = [fqdn]
print format_output(output)
if __name__ == '__main__':
main()