Friday, February 11, 2011

Python + Active Directory + Linux

So, this is really pretty old, but I wanted to share it, since at the time, it took me a while to gather a lot of this information: Managing Active Directory (LDAP) via Linux + Python. There have probably been other posts on this since, but I wanted to put it out there.

I had managed OpenLDAP previously, and then we migrated to Active Directory (Windows Server 2008 at the time). There were a few gotchas that I wasn't expecting.

First, is the default 1,000 entry search results returned by AD. You can change this limit (setting) in Active Directory, but when you think about it, it makes sense to keep it -- if you have a huge directory and are running lots of searches that have huge results, this could definitely thrash your domain controllers.

For your programs to get around this, you need to do use "paged results" (otherwise you'll get an error stating the max returned entries is 1,000).

We are a RHEL shop, but at the time when I worked on this, the default python-ldap package didn't include paged results support (can't remember the version). So, on RHEL / CentOS, grab the latest python-ldap package, and do something like this:

yum remove python-ldap
yum groupinstall "Development Tools"
yum install python-devel
yum install openldap-devel
yum install openssl-devel
tar xvfz python-ldap-2.3.8.tar.gz
cd python-ldap-2.3.8
python setup.py build
python setup.py bdist_rpm
rpm -ivh dist/python-ldap-2.3.8-0.x86_64.rpm

Here is an example in Python of one of those paged searches. I'm pretty sure I grabbed the original from another post somewhere else on the 'net, but I wanted to share it for completeness:

import ldap
from ldap.controls import SimplePagedResultsControl
import sys
import ldap.modlist as modlist

LDAP_SERVER = "ldaps://dc.host.com"
BIND_DN = "Operator@host.com"
BIND_PASS = "password"
USER_FILTER = "(&(objectClass=person)(primaryGroupID=7235))"
USER_BASE = "ou=Special Peeps,ou=My Users,dc=host,dc=com"
PAGE_SIZE = 10

# LDAP connection
try:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, 0)
ldap_connection = ldap.initialize(LDAP_SERVER)
ldap_connection.simple_bind_s(BIND_DN, BIND_PASS)
except ldap.LDAPError, e:
sys.stderr.write('Error connecting to LDAP server: ' + str(e) + '\n')
sys.exit(1)

# Lookup usernames from LDAP via paged search
paged_results_control = SimplePagedResultsControl(
ldap.LDAP_CONTROL_PAGE_OID, True, (PAGE_SIZE, ''))
accounts = []
pages = 0
while True:
serverctrls = [paged_results_control]
try:
msgid = ldap_connection.search_ext(USER_BASE,
ldap.SCOPE_ONELEVEL,
USER_FILTER,
attrlist=['employeeID',
'sAMAccountName'],
serverctrls=serverctrls)
except ldap.LDAPError, e:
sys.stderr.write('Error performing user paged search: ' +
str(e) + '\n')
sys.exit(1)
try:
unused_code, results, unused_msgid, serverctrls = \
ldap_connection.result3(msgid)
except ldap.LDAPError, e:
sys.stderr.write('Error getting user paged search results: ' +
str(e) + '\n')
sys.exit(1)
for result in results:
pages += 1
accounts.append(result)
cookie = None
for serverctrl in serverctrls:
if serverctrl.controlType == ldap.LDAP_CONTROL_PAGE_OID:
unused_est, cookie = serverctrl.controlValue
if cookie:
paged_results_control.controlValue = (PAGE_SIZE, cookie)
break
if not cookie:
break

# LDAP unbind
ldap_connection.unbind_s()

# Make dictionary with user data
user_map = {}
for entry in accounts:
if entry[1].has_key('employeeID') and \
entry[1].has_key('sAMAccountName'):
user_map[entry[1]['employeeID'][0]] = entry[1]['sAMAccountName'][0]

In the above block, I included an example of the connection setup, and retrieving attributes from the result set (not really specific to AD, but someone might find it helpful). Below are some more Python-AD examples, but they are just little snippets (not necessarily complete) of the action.

Changing an Active Directory user's password:

PASSWORD_ATTR = "unicodePwd"
user_dn = user_results[0][1]['distinguishedName'][0]
username = sys.argv[1]
password = getpass.getpass("New password: ")

# Set AD password
unicode_pass = unicode("\"" + password + "\"", "iso-8859-1")
password_value = unicode_pass.encode("utf-16-le")
add_pass = [(ldap.MOD_REPLACE, PASSWORD_ATTR, [password_value])]

# Replace password
try:
ldap_connection.modify_s(user_dn, add_pass)
print "Active Directory password for", username, \
"was set successfully!"
except ldap.LDAPError, e:
sys.stderr.write('Error setting AD password for: ' + username + '\n')
sys.stderr.write('Message: ' + str(e) + '\n')
sys.exit(1)

Create an Active Directory user account:

def CreateUser(username, password, base_dn, fname, lname, domain, employee_num):
"""
Create a new user account in Active Directory.
"""
# LDAP connection
try:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, 0)
ldap_connection = ldap.initialize(LDAP_SERVER)
ldap_connection.simple_bind_s(BIND_DN, BIND_PASS)
except ldap.LDAPError, error_message:
print "Error connecting to LDAP server: %s" % error_message
return False

# Check and see if user exists
try:
user_results = ldap_connection.search_s(base_dn, ldap.SCOPE_SUBTREE,
'(&(sAMAccountName=' +
username +
')(objectClass=person))',
['distinguishedName'])
except ldap.LDAPError, error_message:
print "Error finding username: %s" % error_message
return False

# Check the results
if len(user_results) != 0:
print "User", username, "already exists in AD:", \
user_results[0][1]['distinguishedName'][0]
return False

# Lets build our user: Disabled to start (514)
user_dn = 'cn=' + fname + ' ' + lname + ',' + base_dn
user_attrs = {}
user_attrs['objectClass'] = \
['top', 'person', 'organizationalPerson', 'user']
user_attrs['cn'] = fname + ' ' + lname
user_attrs['userPrincipalName'] = username + '@' + domain
user_attrs['sAMAccountName'] = username
user_attrs['givenName'] = fname
user_attrs['sn'] = lname
user_attrs['displayName'] = fname + ' ' + lname
user_attrs['userAccountControl'] = '514'
user_attrs['mail'] = username + '@host.com'
user_attrs['employeeID'] = employee_num
user_attrs['homeDirectory'] = '\\\\server\\' + username
user_attrs['homeDrive'] = 'H:'
user_attrs['scriptPath'] = 'logon.vbs'
user_ldif = modlist.addModlist(user_attrs)

# Prep the password
unicode_pass = unicode('\"' + password + '\"', 'iso-8859-1')
password_value = unicode_pass.encode('utf-16-le')
add_pass = [(ldap.MOD_REPLACE, 'unicodePwd', [password_value])]
# 512 will set user account to enabled
mod_acct = [(ldap.MOD_REPLACE, 'userAccountControl', '512')]
# New group membership
add_member = [(ldap.MOD_ADD, 'member', user_dn)]
# Replace the primary group ID
mod_pgid = [(ldap.MOD_REPLACE, 'primaryGroupID', GROUP_TOKEN)]
# Delete the Domain Users group membership
del_member = [(ldap.MOD_DELETE, 'member', user_dn)]

# Add the new user account
try:
ldap_connection.add_s(user_dn, user_ldif)
except ldap.LDAPError, error_message:
print "Error adding new user: %s" % error_message
return False

# Add the password
try:
ldap_connection.modify_s(user_dn, add_pass)
except ldap.LDAPError, error_message:
print "Error setting password: %s" % error_message
return False

# Change the account back to enabled
try:
ldap_connection.modify_s(user_dn, mod_acct)
except ldap.LDAPError, error_message:
print "Error enabling user: %s" % error_message
return False

# Add user to their primary group
try:
ldap_connection.modify_s(GROUP_DN, add_member)
except ldap.LDAPError, error_message:
print "Error adding user to group: %s" % error_message
return False

# Modify user's primary group ID
try:
ldap_connection.modify_s(user_dn, mod_pgid)
except ldap.LDAPError, error_message:
print "Error changing user's primary group: %s" % error_message
return False

# Remove user from the Domain Users group
try:
ldap_connection.modify_s(DU_GROUP_DN, del_member)
except ldap.LDAPError, error_message:
print "Error removing user from group: %s" % error_message
return False

# LDAP unbind
ldap_connection.unbind_s()

# Setup user's home directory
os.system('mkdir -p /home/' + username + '/public_html')
os.system('cp /etc/skel/.bashrc /etc/skel/.bash_profile ' +
'/etc/skel/.bash_logout /home/' + username)
os.system('chown -R ' + username + ' /home/' + username)
os.system('chmod 0701 /home/' + username)

# All is good
return True

More to come...

Friday, April 2, 2010

Asterisk and Voicemail Broadcast/Blasting

This is a method I developed this morning for sending out a voicemail message in Asterisk to all users that have a mailbox. I created an extension (in extensions.conf) that allows a user to record a message and then the 'System' application is used to execute a Python script that generates a .call file for all of the users.

I've tested this using Asterisk 1.6.1.1 with about 30 users. This will soon be moving into production on a machine that currently has about 500 mailboxes which will eventually have close to 2,000 mailboxes. I'm not sure what kind of impact this will have on the system load since when using the .call files, it seems like Asterisk tries to push them through as quickly as possible. We'll be sure to try it late at night. =)


/etc/asterisk/extensions.conf
; Context for voicemail blasting system. -- 20100402 MAS
[vm_blast]
exten => _2XXXX,1,Voicemail(${EXTEN},s)


/etc/asterisk/extensions.conf
[default]
; Enter the voicemail blasting system. -- 20100402 MAS
exten => _29999,1,Answer
exten => _29999,2,Wait(1)
exten => _29999,3,Authenticate(1234,j)
exten => _29999,4,Set(RECID=${FILTER(0-9,${UNIQUEID})})
exten => _29999,5,Wait(1)
exten => _29999,6,Playback(dictate/record)
exten => _29999,7,Record(record/vm_blast-${RECID}:gsm)
exten => _29999,8,Wait(1)
exten => _29999,9,Read(CHOICE,vm-review,1)
exten => _29999,10,GotoIf($[${CHOICE} = 1]?13:11)
exten => _29999,11,GotoIf($[${CHOICE} = 2]?16:12)
exten => _29999,12,GotoIf($[${CHOICE} = 3]?5:8)
exten => _29999,13,System(nohup /var/lib/asterisk/vm_blast.py record/vm_blast-${RECID} &)
exten => _29999,14,Playback(auth-thankyou)
exten => _29999,15,Hangup()
exten => _29999,16,Playback(record/vm_blast-${RECID})
exten => _29999,17,Goto(8)


/var/lib/asterisk/vm_blast.py
#! /usr/bin/python

import sys
import os
import tempfile
import time

def main():
# check our arguments
if len(sys.argv) != 2:
print "Must be used with a recording in Asterisk's sound directory!"
sys.exit(1)
# wait a few
time.sleep(10)
# create temp directory
temp_dir = tempfile.mkdtemp(prefix='vm_blast.')
recording = sys.argv[1]
id = recording.split('/')[1]
# get a list of user's that have voicemail boxes
vm_users = os.popen('/usr/sbin/asterisk -rx "voicemail show users for default"')
for line in vm_users.readlines():
extension = line[11:16]
# make sure its a real extension number
if extension.isdigit():
# open our call file
ext_call_file = temp_dir + '/' + id + '-' + extension + '.call'
ext_call_file_handle = open(ext_call_file, 'w')
# write the call file
ext_call_file_handle.write('Channel: Local/' + extension + '@vm_blast\n')
ext_call_file_handle.write('Application: Playback\n')
ext_call_file_handle.write('Data: ' + recording + '\n')
ext_call_file_handle.write('AlwaysDelete: yes\n')
# close our call file
ext_call_file_handle.close()
# finally move our files into the outgoing directory
os.system('mv ' + temp_dir + '/* /var/spool/asterisk/outgoing/')
# remove temp directory
os.system('rm -rf ' + temp_dir)

if __name__ == '__main__':
main()


I've found this tool to be quite useful for formatting code type items: http://formatmysourcecode.blogspot.com/

Monday, November 23, 2009

Gentoo + QLogic's FC HBA Driver

I wanted to use QLogic's FC HBA driver with my Sun StorageTek 6140 disk array on my Gentoo Linux machine, but their driver doesn't seem to be compatible with newer kernels (eg, 2.6.28).

Why use QLogic's proprietary FC HBA driver and not use the open source version included with your kernel? QLogic's driver has MPIO built-in, so no need to use multipath-tools or another MPIO solution. I only see one SCSI device node in /dev per volume. I also like that the firmware image is include in the kernel module, no need for the hotplug framework to load module firmware (binary blob). There is probably a way to "embed" the firmware image into the open source qla2xxx kernel module too, but I haven't researched it.

I hacked the crap out of this driver to make it work on Gentoo and I'll be honest, I'm quite surprised it even works. So, I definitely would not recommend using this driver for production, although I haven't had any problems running in on my machine (yet).

Compiling/installing QLogic's FC HBA (QLE2462 for me) driver on 2.6.28-gentoo-r5:

Get the package from QLogic's web site (qla2xxx-v8.02.23_4-dist.tgz).
tar xvfz qla2xxx-v8.02.23_4-dist.tgz
cd qlogic
./drvrsetup
cd qla2xxx-8.02.23
wget http://longfellow.mcc.edu/~marc.smith/marcitland/qla2xxx-8.02.23.patch
patch -p0 < qla2xxx-8.02.23.patch
extras/build.sh
extras/build.sh install


After the module is installed, a simple 'modprobe qla2xxx' should do the trick. I created an initrd image for my kernel and I can now boot from my SAN.

Wednesday, October 14, 2009

Google Chrome

I love it! My boss showed me Chrome last fall, and I thought great, another browser. Almost a year later and I don't think I could live without it! Its not even beta yet! Well, the Linux port I mean. It runs great, and its not even beta yet (did I mention that)?

Anyhow, I'm running Gentoo/i386, and I use the chromium-bin ebuild in portage to keep up-to-date. Lots of things have improved, however, I still don't have the PDF plugin (Adobe) working correctly. It just opens to a dark grey screen when I click on a PDF. I see this is already reported and open, so I'm waiting on that: http://code.google.com/p/chromium/issues/detail?id=19587

My Java applets haven't been working for a while, and I seen the issue was closed involving Java + Chromium: http://code.google.com/p/chromium/issues/detail?id=16787

It has status fixed, and I never really thought to investigate until today; I just realized it says right in the bug report what needs to be done: Use libnpjp2.so (not libjavaplugin_oji.so).

I guess this is the "new" Java 2 plugin?

So, on my Gentoo machine, to fix it:
rm /usr/lib/nsbrowser/plugins/javaplugin.so
ln -s /usr/share/java-config-2/nsplugin/sun-jdk-1.6-plugin2-javaplugin.so /usr/lib/nsbrowser/plugins/javaplugin.so

Again, that worked for me, your JRE/JDK versions/paths may be different. That also may not be the most elegant way to fix it; I also assume that Gentoo will start using libnpjp2.so by default at some point. Maybe its already that way in current Gentoo? I haven't sync'd / updated in a few weeks.

I did find the "correct" Gentoo way to change the plugin: eselect java-nsplugin set sun-jdk-1.6-plugin2

My First Post

Yay! I have a blog! I never thought I would really get one of these things...
During my short career as an SA, I have come to appreciate the Internet, Google, and all of the information that is available out there to help someone like me. I'd like to give something back; this will be a place for me to post my experiences, tips, tricks, or whatever that deals with computing/IT.

Enjoy!