Working with the lastLogon attribute in PowerShell

I have been spending a lot of time working on Powershell scripts. Specifically, I have been re-writing many VB scripts and processes into more efficient powershell versions. In my last post I mentioned that I planed to share how I made a task that took 8 hours to execute run in only 28 minutes. There are two parts to this efficiency — Start-Job and Group-Object. In a future post I’ll write more about the Start-Job functionality, but the greatest benefit was from Group-Object — and thats what I’ll focus on in this post.

The task I re-wrote looked at about 50,000 user accounts and collected many details about the accounts for auditing purposes. Due to some constraints, I needed to use the lastLogon attribute instead of lastLogonTimeStamp. You can read more about the constraints here: http://www.rlmueller.net/Last%20Logon.htm, but this line pretty much sums it up: Because the lastLogon attribute is not replicated in Active Directory, a different value can be stored in the copy of Active Directory on each Domain Controller. In VB script, I was using an LDAP bind to each domain controller for each user account and then evaluated the lastLogon attribute, which was very inefficient.

Here is the powershell version of this code, which is much more efficient and flexible (as you can get the last login time from each/all domain controllers very easy).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$lastLogons = @()
$domainControllers | %{ # I used an LDAP query for computers with primaryGroupID=516 to get a list of domain controllers distinguished names
    $objDC = New-Object DirectoryServices.DirectoryEntry "LDAP://$_"
    $dcName = $objDC.dnsHostName.ToString()
    $de = New-Object System.DirectoryServices.DirectoryEntry ("LDAP://$dcName")
    $Rech = New-Object System.DirectoryServices.DirectorySearcher($de)
    $Rech.filter = "(&(objectCategory=User)(!objectClass=Computer)(lastLogon>=1))"
    $Rech.SearchScope = "subtree"
    $Rech.sizelimit = "90000"
    $Rech.pagesize = "90000"
    $Rech.PropertiesToLoad.Add("distinguishedName");
    $Rech.PropertiesToLoad.Add("lastLogon");
    $liste = $Rech.FindAll()
    $lastLogons += ($liste | select @{n='DN';e={$_.properties.distinguishedname}},@{n='LastLogon';e={$_.properties.lastlogon}}, @{n='DC';e={$dcName}})
}
 $groupedLastLogin = $lastLogons | Group-Object -Property DN -AsHashTable -AsString
 
# To get an individuals last login information, just select it from the hash table
$trueLastLogin = $groupedLastLogin.Item($objUser.distinguishedName.ToString()) | sort LastLogon -Descending | select -First 1
$lastDC = $trueLastLogin.DC
$lastLogonTrueDate = [datetime]::FromFileTimeUTC($trueLastLogin.lastLogon)

After creating this code, I attempted to select user information from the $lastLogons variable. This was very slow — even slower than the previous VB script that did excessive LDAP binds. The efficiency in this script was gained by grouping the objects into a hash table ($groupedLastLogin) by distinguished name, and then accessing the specific key value as needed.

I know several of my posts have strayed from the normal VMware/vSphere/PowerCLI topics recently, but I have some plans to get back on track early next year. Please stay tuned!

LDAP query performance

The other day I was having a discussion with a co-worker about Active Directory performance related to standard LDAP queries. The complaint (and basis for my involvement in the discussion) was based around poor LDAP performance, which was assumed to be attributed to LDAP queries hitting domain controllers contained within virtual machines. This assumption was based on the fact that some queries completed quickly while similar queries were much slower. After some discussion, and viewing the queries, I believed the problem to be more likely attributed to poorly designed queries, so I set off to prove this with actual data.

The queries included as part of this discussion were searching an attribute that was indexed and part of the partialAttributeSet (replicated to Global Catalogs). The queries that completed faster contained a specific value to search while the slow queries had multiple wildcards included in their value. To prove my hypothesis, I created a simple script to perform the same LDAP searches several times, some with specific values, some with a single wildcard and others with multiple wildcards:

1
2
3
4
5
6
7
8
9
10
$report = @()
for($i=1; $i -le 5; $i++) {
     $item = "" | select Count, SpecificValue, SingleWildcard, TwoWildcards
     $item.count = $i
     $item.SpecificValue = (Measure-Command { get-dn user mail "testuser@test.domain" }).TotalMilliseconds
     $item.SingleWildcard = (Measure-Command { get-dn user mail "testuser*" }).TotalMilliseconds
     $item.TwoWildcards = (Measure-Command { get-dn user mail "*tuser*" }).TotalMilliseconds
     $report += $item
}
$report

*Note: Get-DN is a custom function created by a co-worker. It is a PowerShell function I have loaded in my profile that performs a global catalog search. It builds the LDAP filter using three arguments: 1.) the objectCategory to search 2.) the attribute to search 3.) the value to search for in attribute.

The results of the searchs can be seen below:

Count SpecificValue SingleWildcard TwoWildcards
----- ------------- -------------- ------------
    1        9.5489         9.3398   11066.8601
    2        7.7018          7.335   11102.0755
    3        7.8635         8.3801   11067.5442
    4        8.0664         7.6145   11137.9768
    5       10.2233         8.9622   11132.9646

The averages speak volumes about this test:

Property : SpecificValue
Average  : 8.68078

Property : SingleWildcard
Average  : 8.32632

Property : TwoWildcards
Average  : 11101.48424

As I expected, placing multiple wildcards in an LDAP search greatly impacts search performance — even if the attribute being searched is indexed. What I found somewhat surprising is that a single wildcard has nearly no impact on performance — actually in the test a single wildcard slightly outperformed the search of a specific value (but only by a fraction of a millisecond).

I performed these same queries several times and hard-coded the server names to search to include 1.) physical GC only 2.) virtual GC only 3.) allow DNS to resolve domain name to obtain GC name. Each result set was very similar (within fractions of milliseconds) so I included the default result set where DNS resolves the domain name to obtain a GC.

This article is filed under scripting (and not virtualization) because the results prove that using inefficient queries creates more impact on LDAP directory performance than whether you have physical or virtual domain controllers.

Export, Compare, and Synchronize Active Directory Schemas

I’ve had this link in my notes for a long time. I’ve only used it twice, but thought it was worth sharing. If you ever need to make a test Active Directory for some really crazy testing, and you’ve ever extended the schema, you may want those extended attributes. There is a really good article over at Microsoft Technet that walks through this process: http://technet.microsoft.com/en-us/magazine/2009.04.schema.aspx?pr=blog

I’ll generalize the steps…but this is basically what is going on:

  • Exporting the existing schema using LDIFDE
  • Comparision to another schema using AD DS/LDS Schema Analyzer
  • Exporting the schema differences (using AD DS/LDS Analyzer)
  • Importing the schema differences into the target forest

The instructions are pretty straight forward, so just take a look here if you are interested: http://technet.microsoft.com/en-us/magazine/2009.04.schema.aspx?pr=blog

Get ActiveSync Users

When you have almost 30,000 mailboxes, managing ActiveSync can become a nightmare.  The nice thing about ActiveSync is that is just works.  There is not too much configuration to get it setup, but if you do not plan ahead, you can find yourself in the same predicament as us and have 1500 people connecting through some sort of mobile device without any sort of security policy.  This is a huge security risk, and to combat it we are slowy implementing security policies by agency. So the first step in my process was to get a list of all users that have connected in the last 30 days.

So this way takes a long time if you have lots of mailboxes. For example, this takes over 55 minutes to complete in our environment. But if you do not have a lot of mailboxes, then this method should work fine for you. This report will create a CSV, and list the users name, and type of device.

Get-Mailbox -ResultSize:Unlimited | ForEach {Get-ActiveSyncDeviceStatistics -Mailbox:$_.Identity -ErrorAction SilentlyContinue}| Where{$_.LastSuccessSync -gt '01/01/11'} | Sort-Object -Property DeviceType,Identity | Select-Object @{name="EmailAddress";expression={$_.Identity.ToString().Split("\")[0]}},DeviceType | Export-Csv -Path:"c:\MobileDevices.csv"

One of the bad things about the above script is it will more than likely return some dupilcates. So to combat that, I added a where statement just so I can get the devices that have synced in the last 30 days.

get-mailbox -ResultSize unlimited -Filter {EmailAddresses -like "*@email.com" } | ForEach {Get-ActiveSyncDeviceStatistics -Mailbox:$_.Identity  -ErrorAction SilentlyContinue}| where {$_.lastsuccesssync -gt '02/01/2011'} | Sort-Object -Property DeviceType,Identity | Select-Object @{name="EmailAddress";expression={$_.Identity.ToString().Split("\")[0]}},DeviceType

Ok, now here is a quick way of doing the same thing but by using a filter. I recommend always using a filter to get your results. In this example we are filtering on EmailAddresses and only listing those people that have synced a device in the last 30 days. I spent a few hours trying to figure out how to best do this, and this is the best I could come up with. Please leave a comment if you know of a better way.

get-mailbox -ResultSize unlimited -Filter {EmailAddresses -like "*@email.com" } | ForEach {Get-ActiveSyncDeviceStatistics -Mailbox:$_.Identity  -ErrorAction SilentlyContinue}| where {$_.lastsuccesssync -gt '02/01/2011'} | Sort-Object -Property DeviceType,Identity | Select-Object @{name="EmailAddress";expression={$_.Identity.ToString().Split("\")[0]}},DeviceType | Export-Csv -Path:"c:\MobileDevices_email.com.csv"

Guest bloggers

Good evening, I have been speaking with co-workers about this blog and a few expressed interest in blogging about their day-to-day systems administration duties. In the near future you may see some posts from Steve Kremer. Steve is a co-worker of mine who deals with Active Directory, Citrix and Enterprise Messaging. Steve brings years of experience and a wide array of knowledge.