Skip to content

Instantly share code, notes, and snippets.

@Chaz6
Created October 19, 2012 10:19
Show Gist options
  • Save Chaz6/3917348 to your computer and use it in GitHub Desktop.
Save Chaz6/3917348 to your computer and use it in GitHub Desktop.
Create organizational chart from Active Directory
#!/usr/bin/perl -w
########################################################################################
# This script connects to a domain controller and produces an organizational chart.
#
# It is based on the script at the following page:-
# http://thelowedown.wordpress.com/2008/05/27/generate-calltree-from-active-directory/
#
# Chris Hills (chaz@chaz6.com)
# 2012/10/19
########################################################################################
########################################################################################
# Configuration
########################################################################################
# Domain controller
my $server = "example.com";
# Bind user
my $bindDn = "cn=Administrator,cn=Users,dc=example,dc=com";
# Base Dn
my $baseDn = "cn=Users,dc=example,dc=com";
# Filter
my $filter = "(objectCategory=cn=Person,cn=Schema,cn=Configuration,dc=example,dc=com)";
# Path to graphviz dot
my $DOT = "/usr/bin/dot";
# Output format (supported by dot)
my $format = "svg";
# Output prefix
my ${PREFIX} = "/tmp/ad";
########################################################################################
# Program
########################################################################################
use strict;
use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant ( "LDAP_CONTROL_PAGED" );
$| = 1;
my $trk_loaded = 0;
eval {
require Term::ReadKey;
import Term::ReadKey;
$trk_loaded = 1;
};
print 'Password: ';
ReadMode('noecho') if $trk_loaded; # input echo off
chomp(my $bindPw = $trk_loaded ? ReadLine(0) : <STDIN>);
ReadMode('restore') if $trk_loaded; # input echo on
print "\n";
my $ldap = Net::LDAP->new($server) or die "Cannot connect to LDAP server: $@\n";
my $mesg = $ldap->bind(
dn => $bindDn,
password => $bindPw,
) or die "Cannot bind to ldap: $!\n";;
if($mesg->code){
print "LDAP bind failed: " . $mesg->error_text . "\n";
exit(1);
}
my %calltree;
my %user;
my $cookie;
my $page = Net::LDAP::Control::Paged->new(size => 999) or die $!;
my $attribs = [ "sAMAccountname","displayName","mobile","manager","directReports","distinguishedName","name","telephoneNumber","ipphone","mobile","title","department" ];
my @args = (
base => $baseDn,
scope => 'sub',
filter => $filter,
attrs => $attribs,
pagesize => 800,
control => [$page]
);
print "\n";
print "Searching ldap...\n";
while(1) {
# Perform search
my $mesg = $ldap->search( @args );
if($mesg->code){
print "LDAP search failed: " . $mesg->error_text . "\n";
exit;
}
print "Found " . $mesg->count . " entries...\n";
# Filter results
foreach my $entry ( $mesg->entries ) {
my @rec;
my $DN = $entry->get_value( "distinguishedName" );
my $name = $entry->get_value( "displayName" );
my $acct = lc $entry->get_value( "sAMAccountname" );
my $mgr = $entry->get_value( "manager" );
my $phone = $entry->get_value( "telephoneNumber" ) || $entry->get_value( "ipphone" ) || $entry->get_value( "mobile" );
my $title = $entry->get_value( "title" );
my $dept = $entry->get_value( "department");
# Skip a few special cases
next if lc($name) eq $acct;
if (!defined $mgr) { $mgr = "NONE"; }
if (!defined $phone) { $phone = "UNKNOWN"; }
if (!defined $title) { $title = "UNKNOWN"; }
if (!defined $dept) { $dept = "UNKNOWN"; }
@rec = ($acct, $name, $phone, $mgr, $title, $dept);
# LDAP Attributes are multi-valued, so we have to get each one.
foreach my $subo ( $entry->get_value( "directReports" ) ) {
push (@rec, $subo);
}
$calltree{$DN} = [ @rec ]; # Hash by DN listing attrs and subordinates
$user{$acct} = $DN; # Hash for each account listing the DN
}
# Only continue on LDAP_SUCCESS
$mesg->code and last;
# Get cookie from paged control
my($resp) = $mesg->control( LDAP_CONTROL_PAGED ) or last;
$cookie = $resp->cookie or last;
# Set cookie in paged control
$page->cookie($cookie);
}
if ($cookie) {
# We had an abnormal exit, so let the server know we do not want any more
$page->cookie($cookie);
$page->size(0);
$ldap->search( @args );
die("LDAP query unsuccessful");
}
$ldap->unbind;
print "Finished with LDAP...\n";
my @topGraph;
my @L1Graph;
# Top-level graph: no supervisor or reporting to person w/o supervisor
print "Writing ${PREFIX}-top.txt...\n";
open (DOT, ">${PREFIX}-top.txt") or die "Could not open output file!";
print DOT "digraph CT0 {\n"; # page=\"8.5,11\"\n margin=1\n";
print DOT " rankdir=LR\n";
foreach my $acct (sort keys %user) {
my $entry = $calltree{$user{$acct}};
if ($entry->[3] eq "NONE") {
displayNode( $entry );
push @topGraph, $user{$acct};
if ($#{$entry} > 3) {
for my $i ( 4 .. $#{$entry} ) {
# Only add if listed subordinate is still an employee
if ($calltree{$entry->[$i]}) {
push @L1Graph, $entry->[$i];
displayNode( $calltree{$entry->[$i]} );
print DOT "\t\"$acct\" -> \"$calltree{$entry->[$i]}[0]\"\n";
}
}
}
}
}
print DOT "\n}\n\n";
close DOT;
system( "$DOT -T${format} -o ${PREFIX}-top.$format ${PREFIX}-top.txt" );
my $i = 0;
foreach my $topDN (sort @L1Graph) {
$i++;
print "Writing ${PREFIX}-$i.txt...\n";
open (DOT, ">${PREFIX}-$i.txt") or die "Could not open output file!";
print DOT "digraph CT$i {\n"; # page=\"8.5,11\"\n margin=1\n";
print DOT " rankdir=\"LR\";\n ratio=\"auto\";\n";
traverse( $calltree{$topDN} );
print DOT "}\n\n";
close DOT;
print "Writing ${PREFIX}-$i.$format\n";
system( "$DOT -T$format -o ${PREFIX}-$i.$format ${PREFIX}-$i.txt" );
}
open (DOT, ">${PREFIX}-master.txt") or die "Could not open output file!";
print DOT "digraph CallTree {\n"; # page=\"8.5,11\"\n margin=1\n";
foreach my $acct (sort keys %user) {
my $entry = $calltree{$user{$acct}};
displayNode( $entry );
}
foreach my $acct (sort keys %user) {
my $entry = $calltree{$user{$acct}};
if ($#{$entry} > 3) {
for my $i ( 4 .. $#{$entry} ) {
# Only add if subordinate is still an employee
if ($calltree{$entry->[$i]}) {
displayLink( $entry->[0], $calltree{$entry->[$i]} );
}
}
}
}
print DOT "}\n\n";
close DOT;
print "Writing ${PREFIX}-master.$format\n";
system( "$DOT -T$format -o ${PREFIX}-master.$format ${PREFIX}-master.txt" );
########################################################################################
# Subroutines
########################################################################################
sub traverse {
my $node = shift @_;
displayNode( $node );
if ($#{$node} > 3) {
for $i ( 6 .. $#{$node} ) {
# Only add if mentee is still an employee
if ($calltree{$node->[$i]}) {
traverse( $calltree{$node->[$i]} );
displayLink( $node->[0], $calltree{$node->[$i]} );
}
}
}
}
sub displayNode {
my $node = shift @_;
print DOT "\t\"$node->[0]\" [label=\"$node->[1]\\n$node->[2]\\n$node->[4]\\n$node->[5]\"";
# Display this node differently if no telephoneNumber phone number
if ($node->[2] eq "UNKNOWN") {
print DOT ",shape=box,style=filled,color=\".7 .3 1.0\"";
}
print DOT "];\n";
}
sub displayLink {
my $label = shift @_;
my $next = shift @_;
if (defined $next) {
print DOT "\t\"$label\" -> \"$next->[0]\"\n";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment