By replacing the OpenLDAP XMA with the Søren Granfeldt’s PowerShell MA I gained 20-30% performance improvement, got delta import support, and at the same time reduced the amount of managed code by hundreds of lines.
One of my customers are using OpenDJ as a central LDAP directory for information about users and roles. In order to import this information into FIM we have been using the OpenLDAP XMA from codeplex. But, since this MA is built using the deprecated ECMA 1.0 framework and also have some issues causing me to have to re-write it to a customer specific solution, I decided to move away from it and start using a PowerShell MA instead.
I will try to point out the most critical parts of the PowerShell I used if you would like to try this yourself.
But you can also download a sample script to look at if you like.
This PS MA sends three parameters as input to the PowerShell script
param ($Username,
$Password,
$OperationType)
but also holds a RunStepCustomData
parameter that can be useful when doing delta imports. If you look at the example downloads
listed at this PS MA homepage you will see some examples using this RunStepCustomData
parameter. In this particular case I choose to store the delta cookie in a file instead. Allowing the FIM administrator some flexibility to manually change the cookie value if needed.
Let’s move on to the script then.
I use the System.DirectoryServices.Protocols to do the LDAP queries so I need to start by adding a reference to that assembly.
Add-Type -AssemblyName System.DirectoryServices.Protocols
I need to define the credentials I will use and the LDAP Server to connect to.
$Credentials = New-Object System.Net.NetworkCredential($username,$password)
$LDAPServer = "ldap.company.com"
With that we can now create the LDAP connection to the OpenDJ server.
$LDAPConnection = New-Object System.DirectoryServices.Protocols.LDAPConnection($LDAPServer,$Credentials,"Basic")
Now it’s all about defining the search filters and search the LDAP for the objects we are interested in.It could look something like this
$BaseOU = "ou=People,dc=company,dc=com"
$Filter = "(&(objectClass=EduPerson)(!EduUsedIdentity=*))"
$TimeOut = New-Object System.TimeSpan(1,0,0)
$Request = New-Object System.DirectoryServices.Protocols.SearchRequest($BaseOU, $Filter, "Subtree", $null)
$Response = $LDAPConnection.SendRequest($Request,$TimeOut)
Within the $Response
object we now have the result from our search and can iterate through it and set the values to the object returned to the Management Agent.
ForEach($entry in $Response.Entries) {
$obj = @{}
$obj.Add("OpenDJDN", $entry.DistinguishedName)
$obj.Add("objectClass", "eduPerson")
If($entry.Attributes["cn"]){$obj.Add("cn",$entry.Attributes["cn"][0])}
If($entry.Attributes["uid"]){$obj.Add("uid",$entry.Attributes["uid"][0])}
$obj}
You now have a working import from OpenDJ. But we have not added the delta support just yet. Once you have verified that your full import is working you can start to extend your script to also support delta imports from OpenDJ.
OpenDJ works with a changelog that increases the changenumber for each entry. In order to read the changelog from the correct changenumber we need to store the last known changenumber. In my example I store this in what I call the cookie file.
$CookieFile = "D:\PS-Scripts\OpenDJ\Cookie.bin"
In this file I store the integer value of the last changenumber we have worked with.
To read the value I use the following line.
$val= [int](Get-Content –Path $CookieFile)
We can then search the changelog for entries with higher changeNumber than we have already seen.
It would look something like this.
$ChangeFilter = "(&(targetdn=*$($BaseOU))(!cn=changelog)(changeNumber>=$($changeNumber)))"
$ChangeTimeOut = New-Object System.TimeSpan(0,10,0)
$ChangeRequest = New-Object System.DirectoryServices.Protocols.SearchRequest("cn=changelog", $ChangeFilter, "Subtree", $null)
$ChangeResponse = $LDAPConnection.SendRequest($ChangeRequest,$ChangeTimeOut)
We now run into two problems that need to be resolved.
First: The changelog might give me the same object twice if multiple changes has been done to the object.
Second: The changelog object does not contain all the attibutes I need to return to the MA.
The first problem is solved by filtering the response from the search and only collect unique objects.
In my case I used the following line to do that.
$UniqueDN = $ChangeResponse.Entries | ForEach{$_.attributes["targetdn"][0]} | Get-Unique
We then need to iterate through these unique DNs to get the actual objects and get the attributes we need.
ForEach($DN in $UniqueDN){
$GetUserReq = New-Object System.DirectoryServices.Protocols.SearchRequest($DN, "(objectClass=EduPerson)", "Base", $null)
$GetUser = $LDAPConnection.SendRequest($GetUserReq)
If($GetUser.Entries.Count -eq 0){continue}
$entry = $GetUser.Entries[0]
If($entry.Attributes["EduUsedIdentity"]){continue}
$obj = @{}
$obj.Add("OpenDJDN", $entry.DistinguishedName)
$obj.Add("objectClass", $Class)
If($entry.Attributes["cn"]){$obj.Add("cn",$entry.Attributes["cn"][0])}
If($entry.Attributes["uid"]){$obj.Add("uid",$entry.Attributes["uid"][0])}
$obj}
One thing we need to also remember to do is to save the last changenumber back to our cookie file for the next run.
$LastChangeNumber = [int]$ChangeResponse.Entries[($ChangeResponse.Entries.Count -1)].Attributes["changeNumber"][0]
Set-Content -Value $LastChangeNumber –Path $CookieFile
You also need to remember to do that during your full import runs. So within the script where you process full imports you need to add a search to get the current latest entry in the changelog.
I have attached a demoscript for you to download to get you started in your own exploration of using PowerShell to work with OpenDJ. If you look at it you will likely find that it can be optimized in some ways. But I kept it this way to make it easy for non PowerShell geeks to be able to read it and understand it.
One thing you might have notice is that I have not added support to detect deletes. This is just a matter of adding some logic to read the changetype in the changelog, but I have not yet got the time to do this. At this customer deletes will be detected every night when full import is running anyhow.