Archives for the Month of June, 2011

Organizational Awareness with PowerShell

One of the things that’s always unsettled me a bit in a big organization is keeping yourself informed about changes that you care about, even when those changes never end up making it through the grape vine. For example:

  • Promotions. Of course you want to congratulate them!
  • Transfers. Finding out that a team has moved to another manager, but no “re-org” mail to let you know.
  • Leaving the company. Finding out that a co-worker has left the company or division.

Naturally, PowerShell is here to save the day! I’ve been running a script I call “OrgDiff” as a scheduled task now for a long time – it came about organically, and I look forward to its results every Monday.

Here a couple of great examples of a few little bits of code (75 lines) coming together to make something of surprising utility.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
param($User = 'mybigboss', $Limit = [Int32]::MaxValue, [Switch] $IncludeManager)

$depth = 0

## A list of all the domains that you want to search for
## a given user's alias. Try to organize them in order
## of popularity so that your script runs as quick as possible.

$domains = "LDAP://DC=domain1,DC=corp,DC=contoso,DC=com",
    "LDAP://DC=vendors,DC=corp,DC=contoso,DC=com",
    "LDAP://DC=international,DC=corp,DC=contoso,DC=com"
   
## Find all of the direct reports for a given user
function GetReports($username, $depth
)
{
   
if(-not $username) { return
 }
   
if($depth -gt $Limit) { return
 }

    ## Go through all of the domains and try to find the user
    ## account in that domain
    foreach($domain in $domains
)
    {
       
$adsi = [ADSI] $domain

        $searcher = New-Object System.DirectoryServices.DirectorySearcher $adsi
        $searcher.Filter = "(&(objectClass=User)(mailnickname=$username))"
        $user = $searcher.
FindOne()
       
       
if($user) { break
 }
    }
   
   
if(-not $user
)
    {
       
Write-Error "Could not find $username"
        return
    }

    ## If we want the report to include information about the user's manager,
    ## prepare that portion of the output.
    if($IncludeManager
)
    {
       
$manager = " - " +
            (($user.Properties.manager -split ',')[0] -replace 'CN=',''
)
    }
   
   
## Display the user's name, alias, title, and (optionally) manager -
    ## indented according to how deep we're investigating
    (" " * ($depth * 2)) + $user.Properties.name +
        " (" + $user.Properties.mailnickname +
        ") - $($user.Properties.title)$manager"
   
   
$depth++

    ## If the user has direct reports, call this function again to show
    ## their portion of the organization.
    foreach($directReport in @($user.Properties.
directreports))
    {
        
$report = [ADSI] "GC://$directReport"
         GetReports ($report.mailNickname) $depth
    }
}

GetReports $user $depth

Normal output looks like this:

Some Body (somebody) - GENERAL MANAGER
  Jeffrey Snover (jsnover) - DISTINGUISHED ENGINEER
  Cool Person (coolbeans) - BUSINESS ADMINISTRATOR
  Super Developer (superdev) - DIRECTOR OF DEVELOPMENT
    Dev Manager (devmgr) - DEVELOPMENT MANAGER
      Dev Lead (devlead) _ DEVELOPMENT LEAD
    Dev Manager2 (devmgr2) - DEVELOPMENT MANAGER
(etc)

Now, if you run a weekly script to generate this report into a text file on your hard drive, you can use PowerShell to compare last week’s file with this week’s file. However, when we just compare files line-by-line, you’ll lose the indentation – and therefore the context of who their manager is. That leads to the need for the “-IncludeManager” switch, which generates output more like this:

Some Body (somebody) - GENERAL MANAGER
  Jeffrey Snover (jsnover) - DISTINGUISHED ENGINEER - Some Body
  Cool Person (coolbeans) - BUSINESS ADMINISTRATOR - Some Body
  Super Developer (superdev) - DIRECTOR OF DEVELOPMENT - Some Body
    Dev Manager (devmgr) - DEVELOPMENT MANAGER - Super Developer
      Dev Lead (devlead) _ DEVELOPMENT LEAD - Dev Manager
    Dev Manager2 (devmgr2) - DEVELOPMENT MANAGER - Dev Manager
(etc)

Here's “Compare-OrgChart.ps1”, a script that starts to show useful differences between two files:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
param($BeforePath, $AfterPath)

## Get the content from the first file and the second file, then sort them by name.
## This lets Compare-Object focus primarily on changes to people, and not be
## impacted by random orderings that might come back from Active Directory

$chart1 = Get-Content $BeforePath | % { $_.Trim() } | Sort-Object
$chart2 = Get-Content $AfterPath | % { $_.Trim() } | Sort-Object

## The format of the lines are:
## Super Developer (superdev) - DIRECTOR OF DEVELOPMENT
## So we tell Compare-Object to look for any changes, sort by only their name,
## and after that sort by "Before" then "After"

Compare-Object $chart1 $chart2 |
    Sort { ($_.InputObject -split '-')[0] },SideIndicator

Now, pretend the following happens:

  • Jeffrey gets promoted to Technical Fellow (congrats!)
  • “Dev Manager” leaves the group
  • “Dev Lead” moves under “Dev Manager2”

Here’s the org chart:

Some Body (somebody) - GENERAL MANAGER
  Jeffrey Snover (jsnover) - TECHNICAL FELLOW - Some Body
  Cool Person (coolbeans) - BUSINESS ADMINISTRATOR - Some Body
  Super Developer (superdev) - DIRECTOR OF DEVELOPMENT - Some Body
    Dev Manager2 (devmgr2) - DEVELOPMENT MANAGER - Dev Manager
      Dev Lead (devlead) _ DEVELOPMENT LEAD - Dev Manager2

(etc)

From there, we get this output:

PS C:\Users\Lee> Compare-OrgChart C:\temp\org_before.txt C:\temp\org_after.txt

InputObject                                                   SideIndicator
-----------                                                   -------------
Dev Lead (devlead) _ DEVELOPMENT LEAD - Dev Manager           <=
Dev Lead (devlead) _ DEVELOPMENT LEAD - Dev Manager2          =>
Dev Manager (devmgr) - DEVELOPMENT MANAGER - Super Developer  <=
Jeffrey Snover (jsnover) - DISTINGUISHED ENGINEER - Some Body <=
Jeffrey Snover (jsnover) - TECHNICAL FELLOW - Some Body       =>

That’s pretty cool! Now all we need is a little way to stitch it all together. Here’s a very simple “Update-OrgDiff.ps1” script. All it does is fetch the newest org chart, run the comparison, and email me the results.

001
002
003
004
005
006
007
008
009
010
$mailRecipient = "me@example.com"

$file = "OrgChart-{0}-{1}-{2}.txt" -f (Get-Date).Month,(Get-Date).Day,(Get-Date).Year
Get-OrgChart somebody -IncludeManager > "c:\temp\$file"
$results =  dir c:\temp\OrgChart-* | Sort
60;
LastWriteTime | Select -Last 2
$report = Compare-OrgChart $results[0].FullName $results[1].FullName |
    Format-Table -Auto | Out-String -Width 100

Send-MailMessage -To $mailRecipient -From $mailRecipient -Subject OrgDiff `
   
-BodyAsHtml "<html><body><pre>$report</pre></body></html>" -SmtpServer smtphost

Set this up as a scheduled task, and you’re golden!