Skip to content

Instantly share code, notes, and snippets.

@mbrownnycnyc
Last active December 10, 2015 15:39
Show Gist options
  • Save mbrownnycnyc/4455988 to your computer and use it in GitHub Desktop.
Save mbrownnycnyc/4455988 to your computer and use it in GitHub Desktop.
a resource model script for use with rundeck, queries an active directory ldap server and returns a resource model in XML form ( http://rundeck.org/docs/manpages/man5/resource-v13.html ). Supports LDAPS via START TLS. Not sure if I followed the documented conventions for resource model plugin scripts.
#! /usr/bin/python
# ex: python ldap_resmodel.py -q -u mbrown@domain.local -p 'password_with_a_trailing_backslash\' -b 'OU=Child OU,DC=domain,DC=local' -f
# see python ldap_resmodel.py -h
#this is written to support python 2.6
# on EL, must yum -y install python-ldap
# you must create /usr/lib/python2.*/ldaphelper.py from https://gist.github.com/raw/4453803/ (see http://www.packtpub.com/article/python-ldap-applications-ldap-opearations)
# curl https://gist.github.com/raw/4453803/ > /usr/lib/python2.6/ldaphelper.py
# using TLS (LDAPS):
# Simply: You need to save the issueing CA for LDAPS as /etc/pki/tls/certs/ca_issuer_cert.pem
# Also see: http://technet.microsoft.com/en-us/library/cc778124(WS.10).aspx
#
# openssl version -d # returns the directory where you want to save the pem to in the following:
#
# To obtain and verify the LDAPS certificate:
#
# echo "" | openssl s_client -connect LDAPSSERVER:636 -showcerts 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' > /etc/pki/tls/certs/imported.pem
# ln -s /etc/pki/tls/certs/imported.pem /etc/pki/tls/certs/$(openssl x509 -noout -hash -in /etc/pki/tls/certs/imported.pem).0
# openssl verify -verbose /etc/pki/tls/certs/imported.pem
# # you will receive the following error because you are missing the "certificate issuer" aka CA certificate
# # "unable to get local issuer certificate"
# # It doesn't appear that the LDAPS server on Windows 2003 (at least) presents the CA certificate during connection (as with this: http://serverfault.com/questions/225449/ssl-certificate-error-verify-errornum-20unable-to-get-local-issuer-certificat).
# # Therefore, you must "manually" obtain the Base64 X.509 for the issuer and store it in this case /etc/pki/tls/certs/ca_issuer_cert.pem
# openssl verify -CAfile /etc/pki/tls/certs/ca_issuer_cert.pem -verbose /etc/pki/tls/certs/imported.pem
# Note:
# This script produces a comma-separated list of servicePrincipalName attributes.
# sources:
# http://rundeck.org/docs/manual/plugins.html#script-resource-model-source-configuration
# https://gist.github.com/1288159
# http://stackoverflow.com/a/11706378/843000
# http://www.packtpub.com/article/python-ldap-applications-ldap-opearations ldaphelper.py improved error handling and structure
# http://stackoverflow.com/a/9923346/843000
# http://postneo.com/projects/pyxml/
# http://rundeck.org/docs/manpages/man5/resource-v13.html
# https://bugzilla.redhat.com/show_bug.cgi?id=800787 #TLS error -8179
# http://python-ldap.sourcearchive.com/documentation/2.3.10-1/initialize_8py-source.html tls
# http://serverfault.com/questions/225449/ssl-certificate-error-verify-errornum-20unable-to-get-local-issuer-certificat LDAPS and AD
import sys, ldap, getpass, re
import ldaphelper
from ldaphelper import *
from decimal import *
from xml.dom.minidom import Document
from optparse import OptionParser
parser = OptionParser()
#the options are all strings and accept whatever is after them... meaning using -q is a problem
#parser.add_option('-h', '--help', action='store_true', help='will display this message.')
parser.add_option('-q', '--query', action='store_true', help='will execute a dig for ldapserver and ldapport by looking up the _ldap._tcp SRV record (disregards -s and -p)')
parser.add_option('-s', '--ldapserver', help='ldap server fqdn/hostname (automatically attempts ldaps first)')
parser.add_option('-t', '--ldapport', help='ldap server tcp port (default is 389)')
parser.add_option('-b', '--ldapbasedn', help='ldap instance base designated name, as this script will search all of the tree below (domain.local would be dc=domain,dc=local)')
parser.add_option('-u', '--binduser', help='ldap binding user with FQDN (user@domain.local)')
parser.add_option('-p', '--bindpassword', help='ldap binding user password (or enter password during execution)')
parser.add_option('-f', '--ldaps', action='store_true', help='forces ldaps (TLS) connection, exit if unavailable. Combining with --query will result in attempt LDAPS against the port specified, and will fail if TLS is unavailable.')
parser.add_option('-e', '--executionuser', help='assigns username in output resource model XML.')
options, args = parser.parse_args()
errors = []
error_msg = 'No %s specified. Use option %s'
#print help if no arguments are given
if len(sys.argv) <= 1:
sys.stderr.write( parser.format_help().strip() )
sys.exit(0)
if not options.query:
if not options.ldapserver:
errors.append(error_msg % ('--ldapserver [fqdn]', '-s or --ldapserver or specify --query'))
elif not options.ldapport:
errors.append(error_msg % ('--ldapport [port]', '-t or --ldapport or specify --query'))
if not options.ldapbasedn:
errors.append(error_msg % ('--ldapbasedn [basedn]', '-b or --ldapbasedn'))
if not options.binduser:
errors.append(error_msg % ('--user [bind username with fqdn]', '-u or --user'))
if not options.bindpassword:
options.bindpasswordword = getpass.getpass("Enter the password for %s: " % options.binduser)
if not options.bindpassword:
errors.append(error_msg % ('--password [bind username password]', '-p or --password'))
if not options.executionuser:
errors.append(error_msg % ('--executionuser [username]', '-e or --executionuser'))
#option.query not required
if options.query:
if options.binduser.find('@') > -1:
import subprocess
#execute a dig for _ldap._tcp and get the host info
#dig -t SRV _ldap._tdp.(option.
fqdn = options.binduser[options.binduser.find('@')+1:]
dug = subprocess.Popen(['dig', '-t', 'SRV', '_ldap._tcp.'+fqdn, '+short'], stdout=subprocess.PIPE).communicate()[0]
if len(dug.split('\n')) > 1:
sys.stderr.write("WARNING dig dug " + str(len(dug.split('\n'))) + " dns SRV records for _ldap._tcp." + fqdn + ". Using the first returned.\n")
dug = dug.split('\n')
options.ldapserver = dug[0].split()[3][:len(dug[0].split()[3])-1]
sys.stderr.write("INFO using ldap server: " + options.ldapserver + "\n")
options.ldapport = dug[0].split()[2]
sys.stderr.write("INFO using ldap port : " + options.ldapport + "\n")
else:
errors.append(\
error_msg % ('--query [bind username with fqdn as user@domainname.tld]', '--query only when qualifying user entity with fqdn in user@domainname.tld'))
#you do not need to handle assigning an ldapport, as python-ldap will use default ports.
# in order to support a variable port, we will assign default ports anyway.
#option.forceldaps not required
if options.ldaps:
if len(options.ldapport) == 0:
options.ldapport = '636'
if options.ldapport:
if len(options.ldapport) == 0:
options.ldapport = '389'
#using ldap with ldap_client.start_tls_s() function to use TLS
ldapconstr = 'ldap://'+options.ldapserver+':'+options.ldapport
if errors:
sys.stderr.write('\n'.join(errors) + "\n")
sys.exit(1)
try:
#ldap.set_option(ldap.OPT_DEBUG_LEVEL,0)
#ldapmodule_trace_level = 1
#ldapmodule_trace_file = sys.stderr
#ldap_client = ldap.initialize(ldapconstr,trace_level=ldapmodule_trace_level,trace_file=ldapmodule_trace_file)
ldap_client = ldap.initialize(ldapconstr)
sys.stderr.write("INFO initialized: " + ldapconstr + "\n")
if options.ldaps:
ldap_client.protocol_version=ldap.VERSION3
ldap_client.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
# uncomment the following and comment out the other lines in this block to drop server authenticity checks provided by TLS
#sys.stderr.write("WARNING code maintains no server authenticity. The server may be a different server."+ "\n")
#ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) # allow invalid cert
#check if /etc/pki/tls/certs/imported.pem exists
try:
with open('/etc/pki/tls/certs/ca_issuer_cert.pem') as f: pass
sys.stderr.write("INFO found file at /etc/pki/tls/certs/ca_issuer_cert.pem" + "\n")
except IOError as e:
sys.stderr.write("\n\n" + '\033[01;31m' + "CRITICAL " + '\033[0;0m' + "ldaps attempt without the file /etc/pki/tls/certs/ca_issuer_cert.pem present (should contain the issueing CA X.509 cert in Base64)." + "\n")
sys.stderr.write("try: `echo "" | openssl s_client -connect LDAPSSERVER:636 -prexit 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' > /etc/pki/tls/certs/imported.pem`" + "\n")
sys.exit(0)
sys.stderr.write("INFO setting the cert requirements globally" + "\n")
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) # allow invalid cert
sys.stderr.write("INFO configuring which cert to use" + "\n")
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,'/etc/pki/tls/certs/ca_issuer_cert.pem') #set path to ca cert file
sys.stderr.write("INFO running python-ldap.start_tls_s()" + "\n")
ldap_client.start_tls_s()
sys.stderr.write("INFO issued synchronous connection to negotiate tls to: " + ldapconstr + "\n")
ldap_client.set_option(ldap.OPT_REFERRALS,0)
ldap_client.simple_bind_s(options.binduser, options.bindpassword)
sys.stderr.write("INFO synchronously bound with: " + options.binduser + "\n")
# perform a synchronous search, async and sync with timeout are also available: search(), search_st()
sys.stderr.write("INFO synchronously searching the entire subtree: " + options.ldapbasedn + "\n")
raw_ldap_results = ldap_client.search_s(options.ldapbasedn, ldap.SCOPE_SUBTREE, '(objectClass=computer)', ['dNSHostname','operatingSystem','operatingSystemVersion','operatingSystemServicePack','servicePrincipalName','description'])
clean_ldap_results = ldaphelper.get_search_results( raw_ldap_results )
sys.stderr.write("INFO number of LDAPSearchResult records in search result: "+str(len(clean_ldap_results))+"\n\n")
sys.stderr.write("INFO producing xml structure for query to stdout:" + "\n\n\n")
#referring to http://rundeck.org/docs/manpages/man5/resource-v13.html
# for each found object return and spit out:
# property = "objectClass computer".attribute
# to be included with <node />
# name = dNSHostName
# hostname = dNSHostName
# username = The username used for the remote connection. [probably should accept this as an argument passed into the script]
# to be included as <attribute ...> within <node> tag
# description = description
# osArch = $((operatingSystem contains 'Windows') AND (operatingSystemVersion{$1} > 5.1), then return 'x86_64'; else return 'x86')
# osFamily = $(if operatingSystem contains 'Windows', then return 'windows')
# osName = operatingSystem
# osVersion = $(operatingSystemVersion.substr(' ','').substr('(','.').substr(')',''))
# osServicePack = operatingSystemServicePack
# servicePrincipalName = servicePrincipalName
xmldoc = Document()
project = xmldoc.createElement("project")
xmldoc.appendChild(project)
#note that record.get_attr_values() returns a list... str(record.get_attr_values())="['attribute value']" (attribute value may be a list)
for record in clean_ldap_results:
node = xmldoc.createElement("node")
node.setAttribute("name", str(record.get_attr_values('dNSHostname')[0]))
node.setAttribute("hostname", str(record.get_attr_values('dNSHostname')[0]))
node.setAttribute("username", options.executionuser)
project.appendChild(node)
node_attribute_values = {}
node_attribute_values['description'] = str(record.get_attr_values("description")[0]).lower()
#this doesn't account for ia64 based arch and doesn't properly report 2003 64 bit (since you can't conclude this from the default info stored in AD)
os = str(record.get_attr_values("operatingSystem")[0])
osversion = str(record.get_attr_values("operatingSystemVersion")[0])
if ( os.find("Windows", 0) > -1 ):
node_attribute_values['osFamily'] = 'windows'
node_attribute_values['osVersion'] = osversion[:osversion.find(' (')] + '.' + osversion[(osversion.find(' (')+2):-(len(osversion)-osversion.find(')'))]
node_attribute_values['osServicePack'] = str(record.get_attr_values("operatingSystemServicePack")[0])
node_attribute_values['servicePrincipalName'] = record.get_attr_values("servicePrincipalName")
if type(node_attribute_values['servicePrincipalName']) == list:
node_attribute_values['servicePrincipalName'] = ', '.join(node_attribute_values['servicePrincipalName'])
temposver = Decimal(osversion[:(osversion.find(" ",0))])
if os.find("XP", 0) and ( temposver == Decimal('5.1') ) or os.find("2003", 0) and ( temposver == Decimal('5.2') ):
node_attribute_values['osArch'] = 'x86'
else:
node_attribute_values['osArch'] = 'x86_64'
else:
node_attribute_values['osFamily'] = ''
node_attribute_values['osName'] = os.lower()
#actually add the values to the xmldoc
for attribute in node_attribute_values.keys():
nodeattrib = xmldoc.createElement("attribute")
nodeattrib.setAttribute("name", attribute)
nodeattrib.setAttribute("value", node_attribute_values[attribute])
node.appendChild(nodeattrib)
sys.stdout.write(xmldoc.toprettyxml(indent=" "))
#sys.stdout.write(xmldoc.toxml)
except ldap.LDAPError:
sys.stderr.write(str(sys.exc_type)+"\n")
sys.stderr.write(str(sys.exc_value)+"\n")
sys.exit(1)
finally:
ldap_client.unbind()
sys.exit(0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment