#!/usr/bin/python

import base64
import hashlib
import ldap
import ldap.modlist
import optparse
import os
import re
import subprocess
import sys
import traceback


def exShellCommand(argArray, processInput=''):
	""" Wrapper to run and return output of a shell command """
	
	try:
		proc = subprocess.Popen(argArray,
			                    shell=False,
			                    stdin=subprocess.PIPE,
			                    stdout=subprocess.PIPE,
			                    stderr=subprocess.PIPE)
	
		procStdOut, procStdErr = proc.communicate(processInput)
		rc = proc.returncode

		#if rc:
		#	raise Exception('exShellCommand error ' + str(argArray) + '>>>>' + procStdErr + '<<<<')

	except Exception, ex:	
		raise Exception('exShellCommand error ' + str(argArray) + ' ' + str(ex) + "\n" + traceback.format_exc())

	return rc, procStdOut, procStdErr

 
def removeOlcAccessEntry(dbnum):
	"""Remove any previous olcAccess entires for specified database"""
	
	ldifData = ("dn: olcDatabase={%s}bdb,cn=config\n"
	            "changetype: modify\n"
	            "delete: olcAccess")

	ldifData = ldifData % dbnum

	rc, procOutput, procError = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
	if rc: 
		print procOutput 
		print procError


def change_olcSuffix(dbnum, newSuffix):
	"""Modify the olcSuffix for the specified DB number"""
	
	ldifData = ("dn: olcDatabase={%s}bdb,cn=config\n"
	            "changetype: modify\n"
	            "replace: olcSuffix\n"
	            "olcSuffix: %s\n"
	            "-\n"
	            "replace: olcRootDN\n"
	            "olcRootDN: cn=Manager,%s")

	ldifData = ldifData % (dbnum, newSuffix, newSuffix)

	rc, procOutput, procError = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
	if rc: 
		print procOutput 
		print procError
	else: 
		print procOutput


def remove_olcRootPW(dbnum):
	"""Remove the olcRootPW entry for the specified DB number"""
	
	ldifData = ("dn: olcDatabase={%s}bdb,cn=config\n"
	            "changetype: modify\n"
	            "delete: olcRootPW")

	ldifData = ldifData % (dbnum)
	
	rc, procOutput, procError = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
	if rc: 
		print procOutput 
		print procError
	else: 
		print procOutput


def getDBNumber():
	
	rc, procOutput, procErr = exShellCommand(['slapcat', '-b', 'cn=config', '-a', '(objectClass=olcDatabaseConfig)'])
		
	# find the number of the min olcDatabase entries that has the format bdb 
	minbdbDBNumber = 999
	for m in re.finditer("olcDatabase=\{(\d+)\}bdb", procOutput):
		if int(m.group(1)) < minbdbDBNumber:
			minbdbDBNumber = int(m.group(1))

	print min

	# find the number of the min olcDatabase entries that has the format hdb 
	minhdbDBNumber = 999
	for m in re.finditer("olcDatabase=\{(-?\d+)\}hdb", procOutput):
		if int(m.group(1)) < minhdbDBNumber:
			minhdbDBNumber = int(m.group(1))


	# This way, if all databses use hdb or all use bdb, or even if there is a mix
	# we take the lowest (first) one
	if minbdbDBNumber <	minhdbDBNumber:
		return minbdbDBNumber
	else:
		return minhdbDBNumber


def addolcAccessEntries(dbnum, suffix):

	# our access rules	
	ldifData = ('dn: olcDatabase={%1}bdb,cn=config\n'
		            'changetype: modify\n'
		            'add: olcAccess\n'
		            'olcAccess: {0}to dn.one="%2"\n'
		            '  filter=(objectClass=simpleSecurityObject) attrs=userPassword\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%2" read\n'
		            '  by anonymous auth\n'
		            '  by * none\n'
		            'olcAccess: {1}to dn.one="ou=users,%2"\n'
		            '  filter=(objectClass=posixAccount) attrs=userPassword\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%2" read\n'
		            '  by anonymous auth\n'
		            '  by * none\n'
		            'olcAccess: {2}to dn.one="ou=users,%2"\n'
		            '  filter=(objectClass=posixAccount)\n'
		            '  attrs=entry,objectClass,cn,gecos,gidNumber,homeDirectory,loginShell,uid,uidNumber\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%2" read\n'
		            '  by dn="cn=search,%2" read\n'
		            '  by self read\n'
		            '  by * none\n'
		            'olcAccess: {3}to dn.one="ou=users,%2"\n'
		            '  filter=(&(objectClass=posixAccount)(objectClass=shadowAccount))\n'
		            '  attrs=shadowExpire,shadowFlag,shadowInactive,shadowLastChange,shadowMax,shadowMin,shadowWarning\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%2" read\n'
		            '  by dn="cn=search,%2" read\n'
		            '  by * none\n'
		            'olcAccess: {4}to dn.one="ou=users,%2"\n'
		            '  filter=(objectClass=posixAccount)\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%2" read\n'
		            '  by * none\n'
		            'olcAccess: {5}to dn.one="ou=groups,dc=%2"\n'
		            '  filter=(objectClass=posixGroup)\n'
		            '  attrs=entry,objectClass,cn,gidNumber,memberUid,userPassword\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="dc=dev30cn=update,%2" read\n'
		            '  by dn="cn=search,%2" read\n'
		            '  by * none\n'
		            'olcAccess: {6}to dn.one="ou=groups,%2"\n'
		            '  filter=(objectClass=posixGroup)\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%2" read\n'
		            '  by * none\n'
		            'olcAccess: {7}to dn.subtree="%2"\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%2" read\n'
		            '  by dn="cn=search,%2" search\n'
		            '  by * none')
	
	ldifData =  ldifData.replace('%1', dbnum)
	ldifData =  ldifData.replace('%2', suffix)
	
	print "olcAccess" + ldifData
	
	rc, procOutput, procError = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
	if rc: 
		print procError
		print procOutput 


def defaultSetup(suffix, ldapManagerUserDN='', ldapPW=''):
	
	rc, procOutput, procErr = exShellCommand(['slapcat', '-b', 'cn=config', '-a', '(objectClass=olcDatabaseConfig)'])
	
	# Well use this later
	fullSlapcatOutput = procOutput
	
	# find the number of the max olcDatabase entries
	maxDBNumber = -9999
	for m in re.finditer("olcDatabase=\{(-?\d+)\}", procOutput):
		if m.group(1) > maxDBNumber:
			maxDBNumber = m.group(1)
	
	# find the oldDbDirectory for the max db (this should normally be a 2)
	rc, procOutput, procErr = exShellCommand(['slapcat', '-b', 'cn=config', '-a', '(&(objectClass=olcDatabaseConfig)(olcDatabase={' + maxDBNumber + '}bdb))'])
	
	matchObj = re.search(r'olcDbDirectory: (.*)$' , procOutput, re.M)
	if matchObj:
		print 'found olcDbDirectory for for olcDatabase={' + str(maxDBNumber) + '}bdb: ' + matchObj.group(1)
		configFileDir = matchObj.group(1)
	else:
		print 'Error, cannot find an entry for olcDbDirectory for (olcDatabase={' + str(maxDBNumber) + '}bdb)'
		exit(1)
	
	# See if this file exists for this database
	configFileName = configFileDir + '/DB_CONFIG'
	print "checking for " + configFileName

	if not os.path.exists(configFileName):
		print "file not present"
	
		# add the data to ldap
		ldifData = ("dn: olcDatabase={2}bdb,cn=config\n"
		"changetype: modify\n"
		"add: olcDbConfig\n"
		"olcDbConfig: set_cachesize 0 268435456 1\n"
		"olcDbConfig: set_lg_regionmax 262144\n"
		"olcDbConfig: set_lg_bsize 2097152")
	
		rc, procOutput, procError = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
		if rc: 
			print procOutput 
			print procError
		else: 
			print procOutput

		# restarting the server should generate the file we were looking for from the attribute mods we did above
		rc, procOutput, procError = exShellCommand(['service', 'slapd', 'restart'])
	else:
		print "file exists, no need to create"

	
	# See if our ldap db is empty
	rc, procOutput, procErr = exShellCommand(['slapcat', '-n', str(maxDBNumber)])

	if procOutput:
		print "Current DB does not appear to be empty"
		print procOutput
		exit(-1)
		#todo, something else

	# Since the database was empty, we're assuming it's ok to clear out any existing rules
	print "Removing OlcAccess entires. They might not exist, don't be alarmed by errors complaining about it"
	removeOlcAccessEntry(maxDBNumber)

	# if olcRootPW doesn't exist, make it
	if fullSlapcatOutput.find('olcRootPW:') == -1:
		
		# create a root user for the new database to allow remote connections
		ldifData = ("dn: olcDatabase={%s}bdb,cn=config\n"
		            "changetype: modify\n"
		            "replace: olcRootDN\n"
		            "olcRootDN: cn=Manager,%s\n"
		            "-\n"
		            "add: olcRootPW\n"
		            "olcRootPW: %s")
		
		# since we are setting the pw ourselves, set ldapPW to our value for use throughout the rest of this function
		ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(ldapPW)).digest())
		
		print "ldapPWHash :" + ldapPWHash
		
		# since the pw wasn't specified, we're going to assume this is also a default install where the olcRootDN 
		# is olcRootDN: cn=Manager,<site specific suffix>
		ldapManagerUserDN = 'cn=Manager,' +  suffix
		
		ldifData = ldifData % (maxDBNumber, suffix, ldapPWHash)

		print "adding olcRootPW"
		
		rc, procOutput, procError = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
		if rc: 
			print procError
			print procOutput 
	else:
		print "olcRootPW already exists"
		ldapManagerUserDN = 'cn=Manager,' +  suffix


	# Switch over to native ldap calls for the remainder of the setup using the above user and pw we just setup
	print "attempting bind to localhost"
	print "ldapManagerUserDN:" + ldapManagerUserDN
	print "ldapPW:" + ldapPW

	l = ldap.open("localhost")
	l.simple_bind_s(ldapManagerUserDN, ldapPW)


	# create top level entry
	dn = "dc=hubzero,dc=org"
	attrs = {}
	attrs['objectclass'] = ['top', 'dcObject', 'organization']
	attrs['o'] = 'hub'
	attrs['description'] = 'hubzero hub'
	ldif = ldap.modlist.addModlist(attrs)
	print "adding root container"
	l.add_s(dn, ldif)
	
	# create admin, syncuser, and search users
	dn = "cn=admin,dc=hubzero,dc=org"
	attrs = {}
	attrs['objectclass'] = ['organizationalRole', 'simpleSecurityObject']
	attrs['cn'] = 'admin'
	attrs['userPassword'] = ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(ldapPW)).digest())
	ldif = ldap.modlist.addModlist(attrs)
	print "adding admin user"
	l.add_s(dn, ldif)

	dn = "cn=syncuser,dc=hubzero,dc=org"
	attrs = {}
	attrs['objectclass'] = ['organizationalRole', 'simpleSecurityObject']
	attrs['cn'] = 'syncuser'
	attrs['userPassword'] = ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(ldapPW)).digest())
	ldif = ldap.modlist.addModlist(attrs)
	print "adding sync user"
	l.add_s(dn, ldif)

	dn = "cn=search,dc=hubzero,dc=org"
	attrs = {}
	attrs['objectclass'] = ['organizationalRole', 'simpleSecurityObject']
	attrs['cn'] = 'search'
	attrs['userPassword'] = ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(ldapPW)).digest())
	ldif = ldap.modlist.addModlist(attrs)
	print "adding search user"
	l.add_s(dn, ldif)


	# Add our orgaqnizational units
	dn = "ou=users,dc=hubzero,dc=org"
	attrs = {}
	attrs['objectclass'] = ['organizationalUnit']
	attrs['ou'] = 'users'
	attrs['description'] = 'Users container'
	ldif = ldap.modlist.addModlist(attrs)
	print "adding users root ou container"
	l.add_s(dn, ldif)

	dn = "ou=groups,dc=hubzero,dc=org"
	attrs = {}
	attrs['objectclass'] = ['organizationalUnit']
	attrs['ou'] = 'groups'
	attrs['description'] = 'Groups container'
	ldif = ldap.modlist.addModlist(attrs)
	print "adding groups root ou container"
	l.add_s(dn, ldif)
	
	
	print "unbinding from ldap"
	l.unbind();

	
	


# #######################################################################
# Main


# arg parsing logic
parser = optparse.OptionParser()
parser.add_option("-t", "--test", action="store_true", dest="test", help="just run the test code")
parser.add_option("-s", "--suffix", action="store", dest="suffix", default="localhost", help="ldap suffix, only used when resetting the db suffix")
parser.add_option("-u", "--ldapuser", action="store", dest="ldapuser", default="", help="root user of database if user already exists")
parser.add_option("-p", "--ldappw", action="store", dest="ldappw", default="Hubzero", help="root user pw")
parser.add_option("-d", "--deleterootPW", action="store_true", dest="deleterootPW", default="false", help="root user pw")
parser.add_option("-c", "--changenamecontext", action="store", dest="changenamecontext", default="", help="Change the naming context for the primary ldap database")

(options, args) = parser.parse_args()


if options.test:
	print "running ONLY test code"
	print "done"
else:

	if options.deleterootPW == True:
		print "Deleting olcRootPassword"
		dbnum = getDBNumber()
		remove_olcRootPW(dbnum)
		print "done"
	elif options.changenamecontext:
		print "Modifying db suffix"
		dbnum = getDBNumber()
		change_olcSuffix(dbnum, options.changenamecontext)
	else:
		print "Running default setup"
		defaultSetup(options.suffix, options.ldapuser, options.ldappw);

exit(0)
