Load Balancing User Placement in GroupWise Post Offices - Part 2

0 Likes

This is the second part of an article I started at:
Load Balancing User Placement in GroupWise Post Offices



The core issue is, when you have an Identity Manager driver creating users in GroupWise, you need to decide which Post Office to place the user in. If you only have one GroupWise Post Office, then the default driver configuration is sufficient.



I based my work on an article by David Gersic:
Using the GroupWise Post Office Record Count with IDM



In that article David describes his use case, of needing to make sure each Post Office (he has several) has only 1000 users in each, and fill them up in order. To do that, he shows how to query for the user count on the Post Office in an Identity Manager 3.0 world.



I took the tack (yes, a sailing reference, in one of my other multiple personalities I am a sailing instructor, but only on dinghies. 15 footers at the maximum. No keel boats for me! Real men use centerboards! Though we will tolerate dagger boards as well. Laser 2's are my personal favorites.) of updating the approach to an Identity Manager 3.5 and higher implementation, since it is pretty easy to do, and makes things much simpler.



Now in my first attempt at solving this problem in my own inimitable way, was somewhat convoluted, but quite ironically, what I considered the lazy mans way. I used a GCV to store a Schema attribute name, (like acmePOListAttr), and then read the value of that attribute off the driver object as a nodeset, and then loop through that nodeset.



Convoluted right? But the funny part is, that for me is an easier approach than trying to test and be certain how to do it with another structure, just because I know exactly what will happen, and I use this model all the time elsewhere.



However, a GCV, a schema extension, adding an auxiliary class to the driver object, filling out values without a pretty interface via ConsoleOne, iManager, or LDIF, do not really bother me that much, I realized that might be, as they say, "Less than optimal"



I often find I take a somewhat brute force approach to demonstrate I can make it work, then I work on making it work better, so lets optimize. Sometimes first approaches are not the best approach.



Ok so first question. Where can we store the data the so that we can retrieve it easily via built in functions in Identity Manager? Well first off I guess there are Global Configuration Variables, aka GCV's that can be used. There are a couple of possible approaches there.



Another data structure we have access to in Identity Manager 3.5 and higher is a Mapping table, and a Map token, which can read data out of the mapping table.



We already did custom attribute on an object in eDirectory in the first approach, and off the top of my head I cannot think of many others that make sense.



There is one further wrinkle to this issue, which came up after I worked out my first approach, which is to note that you might have multiple GroupWise Domains for your various Post Offices. So really we need to store two bits of information for each Post Office name. The Post Office, and the Domain to which it belongs. In my first approach I just assumed all the Post Offices were in the same Domain, but there is no real basis to that assumption, and the real world may not be like that.



Easy enough to handle, so lets start with the Mapping table approach since it lends itself to multiple columns of data at the surface level than other possible approaches, it is a table after all.



Load balancing mailboxes across Exchange 2007 message stores using IDM policy rules



I came up with this on my own, before even seeing this idea, but it is an obvious approach, so credit to the guy came up with it before me! The name on the article is 9556613, so I will just call him 9 for short (Having never met this person, no way would I suggest 7 of 9, assuming you like Jeri Ryan. Star Trek joke for those who have no idea what I am talking about. Use Google!)



The general idea is in the mapping table to store the first column called say counter or index, the second column named "po" and the third column named "domain".



The Mapping table would look something like this image.





There is no for (i=1, i , i < 6) style construct in Identity Manager, per se, but you can make one close enough if you want to spend the time.



Here is the rule in its entirety as XML so you can paste it into Designer, and an image of what it looks like in Designer, which is a bit more readable. Then lets start dissecting it to make it understandable.





<rule>
<description>[acme] PO Load balancing placement rule, using mapping table</description>
<comment xml:space="preserve">I want this to be generic and took a second approach. Use a mapping table to store three columns, counter, po, domain. Then start with i=1, do a while loop, get map value from source column counter=1, get po and domain. Increment i, repeat.

Set PO-LIST to values of the attr in the GCV that stores the attr name, read it off the driver object.

If there are no values, use the default value the driver ships with.
If there is a value returned, then set MIN to a high number.

Loop through the PO-LIST nodeset and query where 50062 is the current nodes value (Current PO name we are trying), and the 50035 value is our domain. Read the Record Count attr which should show how many users are in this PO.

If the count is less than MIN, this is our new min. Set MIN to PO-COUNT, set MIN-PO to the current nodes value.

When done looping, set the op'n dest dn to the MIN-PO value. </comment>
<conditions>
<and>
<if-class-name mode="nocase" op="equal">User</if-class-name>
</and>
</conditions>
<actions>
<do-set-local-variable name="I" scope="policy">
<arg-string>
<token-text xml:space="preserve">1</token-text>
</arg-string>
</do-set-local-variable>
<do-set-local-variable name="PO" scope="policy">
<arg-string>
<token-map default-value="XXYY" dest="po" src="counter" table="..\..\[acme] Library\[acme] GW PO list">
<token-local-variable name="I"/>
</token-map>
</arg-string>
</do-set-local-variable>
<do-set-local-variable name="DOMAIN" scope="policy">
<arg-string>
<token-map default-value="XXYY" dest="domain" src="counter" table="..\..\[acme] Library\[acme] GW PO list">
<token-local-variable name="I"/>
</token-map>
</arg-string>
</do-set-local-variable>
<do-set-local-variable name="MIN" scope="policy">
<arg-string>
<token-text xml:space="preserve">99999999999</token-text>
</arg-string>
</do-set-local-variable>
<do-while>
<arg-conditions>
<and>
<if-local-variable mode="nocase" name="PO" op="not-equal">XXYY</if-local-variable>
</and>
</arg-conditions>
<arg-actions>
<do-set-local-variable name="PO-COUNT" scope="policy">
<arg-string>
<token-query class-name="User">
<arg-match-attr name="50062">
<arg-value type="string">
<token-local-variable name="PO"/>
</arg-value>
</arg-match-attr>
<arg-match-attr name="50035">
<arg-value type="string">
<token-local-variable name="DOMAIN"/>
</arg-value>
</arg-match-attr>
<arg-string>
<token-text xml:space="preserve">Record Count</token-text>
</arg-string>
</token-query>
</arg-string>
</do-set-local-variable>
<do-if>
<arg-conditions>
<and>
<if-xpath op="true">number($PO-COUNT) < number($MIN)</if-xpath>
</and>
</arg-conditions>
<arg-actions>
<do-set-local-variable name="MIN" scope="policy">
<arg-string>
<token-local-variable name="PO-COUNT"/>
</arg-string>
</do-set-local-variable>
<do-set-local-variable name="MIN-PO" scope="policy">
<arg-string>
<token-local-variable name="PO"/>
</arg-string>
</do-set-local-variable>
</arg-actions>
<arg-actions/>
</do-if>
<do-set-local-variable name="PO-REPORT" scope="policy">
<arg-string>
<token-local-variable name="PO-REPORT"/>
<token-text xml:space="preserve">
PO Name: </token-text>
<token-local-variable name="PO"/>
<token-text xml:space="preserve"> Number of users: </token-text>
<token-local-variable name="PO-COUNT"/>
</arg-string>
</do-set-local-variable>
<do-set-local-variable name="I" scope="policy">
<arg-string>
<token-xpath expression="number($I) 1"/>
</arg-string>
</do-set-local-variable>
<do-set-local-variable name="PO" scope="policy">
<arg-string>
<token-map default-value="XXYY" dest="po" src="counter" table="..\..\[acme] Library\[acme] GW PO list">
<token-local-variable name="I"/>
</token-map>
</arg-string>
</do-set-local-variable>
<do-set-local-variable name="DOMAIN" scope="policy">
<arg-string>
<token-map default-value="XXYY" dest="domain" src="counter" table="..\..\[acme] Library\[acme] GW PO list">
<token-local-variable name="I"/>
</token-map>
</arg-string>
</do-set-local-variable>
</arg-actions>
</do-while>
<do-set-local-variable name="PO-REPORT" scope="policy">
<arg-string>
<token-local-variable name="PO-REPORT"/>
<token-text xml:space="preserve">
and we added the user to the PO: </token-text>
<token-local-variable name="MIN-PO"/>
</arg-string>
</do-set-local-variable>
<do-set-op-dest-dn>
<arg-dn>
<token-local-variable name="MIN-PO"/>
</arg-dn>
</do-set-op-dest-dn>
<do-trace-message>
<arg-string>
<token-text xml:space="preserve">Shazam! </token-text>
<token-local-variable name="PO-REPORT"/>
</arg-string>
</do-trace-message>
</actions>
</rule>



Please as a general rule, use the Comment field for the rule to discuss what you are doing in the rule. I really want to push this, as there is nothing worse than looking at a complex rule, that has a meaningless name, and no comments, and clearly is doing a lot of work, much of it no doubt very subtle. Especially if you are relying on external objects and interacting with rules elsewhere, please make it a standard practice to reference each part in the comments so that is easier to maintain over time.



First we need to initialize our counter variable, the astonishingly overused "I".



We are going to use a While loop, which usually means you need to set the beginning conditions first. Thus we need to get the first value for PO from the table.



It happens, I made a Library object at the root of the driver set, because I have some ECMA Script function I intend to use in this project, and I will probably have a bunch of rules I reuse via the use of this Library. Also note I made sure to tick the Set DN Relative to Policy box in the Map token, which is why the path to the Library is "..\..\[acme] Library\[acme] GW PO List" what this means is I do not care about the OU structure of the driver or tree, as to where the driver is residing, it is relative to the Driver Set. (See this article for more details: Mapping tables and Render browsed DN relative to policy option)



We use the source column "counter" as the value of I, which is currently 1, to get the value of the column named "po" as the destination. We store that in the PO local variable. No need for a node set here, it is just a string. Also took advantage of a new feature in Identity Manager 3.6, where Map tokens have a new value, called Default. Which means if no value is returned, then return a default value. This is perfect for a While loop, as there you have our end condition pre-built for us! Yay! That makes it trivial to handle, no testing for null or not equal to something, just pick some crazy value that will never be a real Post Office or Domain name and set that as the default value.



<do-set-local-variable name="PO" scope="policy">
<arg-string>
<token-map default-value="XXYY" dest="po" src="counter" table="..\..\[acme] Library\[acme] GW PO list">
<token-local-variable name="I"/>
</token-map>
</arg-string>
</do-set-local-variable>



Do the same thing for DOMAIN local variable, this time source is still "counter" for the value of I, but destination is the "domain" column.



<do-set-local-variable name="DOMAIN" scope="policy">
<arg-string>
<token-map default-value="XXYY" dest="domain" src="counter" table="..\..\[acme] Library\[acme] GW PO list">
<token-local-variable name="I"/>
</token-map>
</arg-string>
</do-set-local-variable>



Great, so now either we have a value for the PO and DOMAIN local variables of real Post Office and Domain combinations, or else we have XXYY (my silly default value).



Lets set local variable MIN to some silly high number, and then we are ready to start our While loop.



Our end condition for the While loop is simple, While variable PO is not equal to XXYY the default value, keep going.



If we get past that, we have a real value from the table, so lets do the same query as we used in the first part of this article, and see what value we get back for number of users in the Post Office specified by the PO variable, and the Domain specified by the DOMAIN variable.



<do-set-local-variable name="PO-COUNT" scope="policy">
<arg-string>
<token-query class-name="User">
<arg-match-attr name="50062">
<arg-value type="string">
<token-local-variable name="PO"/>
</arg-value>
</arg-match-attr>
<arg-match-attr name="50035">
<arg-value type="string">
<token-local-variable name="DOMAIN"/>
</arg-value>
</arg-match-attr>
<arg-string>
<token-text xml:space="preserve">Record Count</token-text>
</arg-string>
</token-query>
</arg-string>
</do-set-local-variable>



As before, test if the current value is less than our current MIN (which is very high right now, so should always be true on the first time through the loop).



<if-xpath op="true">number($PO-COUNT) < number($MIN)</if-xpath>


In this case, we know it is, so we reset MIN to be the current PO-COUNT value, and we store the Post Office Name that is the lowest count so far, as MIN-PO.



For troubleshooting purposes, lets record the values we returned from our query in a variable, that we can email or display later to see that everything happened the way we want.



Then we have to increment I, so that next loop through we read the next value in the Mapping Table.



<do-set-local-variable name="DOMAIN" scope="policy">
<arg-string>
<token-map default-value="XXYY" dest="domain" src="counter" table="..\..\[acme] Library\[acme] GW PO list">
<token-local-variable name="I"/>
</token-map>
</arg-string>
</do-set-local-variable>



Then we get the next values of PO and DOMAIN from the mapping table, repeating the exact from before, and since we do it just after we increment I, we get the next value from the table.



Finally, when the loop is done, we record into our PO-REPORT variable the value we decided on after looping.



Set the operation destination DN, to the Post Office name we stored in MIN-PO:



<do-set-op-dest-dn>
<arg-dn>
<token-local-variable name="MIN-PO"/>
</arg-dn>
</do-set-op-dest-dn>



Last but not least, lets trace it to the screen, with something silly so we can find it in trace much easier.



That approach is probably the best way. One upside to it, is that changes to a Mapping table take affect on the next event. No driver reload needed. No code changes in the driver is required to add a new Post Office into the load balancing model. No reload would be needed for my first attempt, using an attribute on the driver object either.



The next approach is basically identical to my first approach, but instead of reading the attribute with the list of Post Office names from an attribute on the driver into a nodeset, read a GCV that is defined as a list GCV.



If you look back at the rule I posted in the first article, the only real change is the first action item, which was:



<do-set-local-variable name="PO-LIST" scope="policy">
<arg-node-set>
<token-src-attr name="~gw.POListAttrName~">
<arg-dn>
<token-global-variable name="dirxml.auto.driverdn"/>
</arg-dn>
</token-src-attr>
</arg-node-set>
</do-set-local-variable>



Instead we modify it look more like:



<do-set-local-variable name="PO-LIST" scope="policy">
<arg-node-set>
<token-global-variable name="gw.driver.POList"/>
</arg-node-set>
</do-set-local-variable>




The GCV gw.driver.POList definition is something like:



<definition display-name="List GCV with the PO's we will load balance across" name="gw.driver.POList" type="list">
<description></description>
<value>
<item>PO1</item>
<item>PO2</item>
<item>PO3</item>
</value>
</definition>


However, if you change a value, you will need a driver reload.



There is one further tricky bit that will be left as an exercise to the reader, mostly because I am bored with the topic already, which is to how to specify a different Domain with each Post Office?



Basically how to get a two dimensional array of values from a GCV. Probably the easiest way is to comma separate the values, so in the list GCV, each entry becomes something like PO1,DOMAIN1 then PO2,DOMAIN2 and so on. Then as you loop through the values of the nodeset, use the Split token, based on a comma as a separator. This would store it in a nodeset with two nodes, one for the first part, one for the second.



Or you could use the XPATH function substring-before($current-node,",") and substring-after($current-node,",") to break it up.



If you are working on this, and solve it, post an article in response and lets see your solution!



Edit Apr 1, 2009: Clearly no one tried this sample code, as I had a glaring error in it. Fixed now. I converted a for-each loop to a while loop, and left behind a pair of references to the local variable current-node. My bad. Also, the do-set-op-dest-dn will not quite work exactly as shown. You should actually set the eDirectory DN of the PO object, which I just wrote into a fourth column of the mapping table and read out to get in that rule instead. (I am not good in this editor with code samples, else I would post the better sample. Turns out that the dest-dn is a shortcut for the Domain/PO combination of attributes and the driver converts the info from the eDirectory object to the two needed attributes in GW.


Labels:

How To-Best Practice
Comment List
Related
Recommended