1#!/usr/bin/perl -w
2
3# Brian Masney <masneyb@gftp.org>
4# To use this script, set your base DN below. Then run 
5# ./dhcpd-conf-to-ldap.pl < /path-to-dhcpd-conf/dhcpd.conf > output-file
6# The output of this script will generate entries in LDIF format. You can use
7# the slapadd command to add these entries into your LDAP server. You will
8# definately want to double check that your LDAP entries are correct before
9# you load them into LDAP.
10
11# This script does not do much error checking. Make sure before you run this
12# that the DHCP server doesn't give any errors about your config file
13
14# FailOver notes:
15#   Failover is disabled by default, since it may need manually intervention.
16#   You can try the '--use=failover' option to see what happens :-)
17#
18#   If enabled, the failover pool references will be written to LDIF output.
19#   The failover configs itself will be added to the dhcpServer statements
20#   and not to the dhcpService object (since this script uses only one and
21#   it may be usefull to have multiple service containers in failover mode).
22#   Further, this script does not check if primary or secondary makes sense,
23#   it simply converts what it gets...
24
25use Net::Domain qw(hostname hostfqdn hostdomain);
26use Getopt::Long;
27
28my $domain = hostdomain();           # your.domain
29my $basedn = "dc=".$domain;
30   $basedn =~ s/\./,dc=/g;           # dc=your,dc=domain
31my $server = hostname();             # hostname (nodename)
32my $dhcpcn = 'DHCP Config';          # CN of DHCP config tree
33my $dhcpdn = "cn=$dhcpcn, $basedn";  # DHCP config tree DN
34my $second = '';                     # secondary server DN / hostname
35my $i_conf = '';                     # dhcp.conf file to read or stdin
36my $o_ldif = '';                     # output ldif file name or stdout
37my @use    = ();                     # extended flags (failover)
38
39sub usage($;$)
40{
41  my $rc = shift;
42  my $err= shift;
43
44  print STDERR "Error: $err\n\n" if(defined $err);
45  print STDERR <<__EOF_USAGE__;
46usage: 
47  $0 [options] < dhcpd.conf > dhcpd.ldif
48
49options:
50
51  --basedn  "dc=your,dc=domain"        ("$basedn")
52
53  --dhcpdn  "dhcp config DN"           ("$dhcpdn")
54
55  --server  "dhcp server name"         ("$server")
56
57  --second  "secondary server or DN"   ("$second")
58
59  --conf    "/path/to/dhcpd.conf"      (default is stdin)
60  --ldif    "/path/to/output.ldif"     (default is stdout)
61
62  --use     "extended features"        (see source comments)
63__EOF_USAGE__
64  exit($rc);
65}
66
67
68sub next_token
69{
70  local ($lowercase) = @_;
71  local ($token, $newline);
72
73  do 
74    {
75      if (!defined ($line) || length ($line) == 0)
76        {
77          $line = <>;
78          return undef if !defined ($line);
79          chop $line;
80          $line_number++;
81          $token_number = 0;
82        }
83
84      $line =~ s/#.*//;
85      $line =~ s/^\s+//;
86      $line =~ s/\s+$//;
87    }
88  while (length ($line) == 0);
89
90  if (($token, $newline) = $line =~ /^(.*?)\s+(.*)/)
91    {
92      if ($token =~ /^"/) {
93       #handle quoted token
94       if ($token !~ /"\s*$/)
95       {
96         ($tok, $newline)  = $newline =~ /([^"]+")(.*)/;
97         $token .= " $tok";
98        }
99      }
100      $line = $newline;
101    }
102  else
103    {
104      $token = $line;
105      $line = '';
106    }
107  $token_number++;
108
109  $token =~ y/[A-Z]/[a-z]/ if $lowercase;
110
111  return ($token);
112}
113
114
115sub remaining_line
116{
117  local ($block) = shift || 0;
118  local ($tmp, $str);
119
120  $str = "";
121  while (defined($tmp = next_token (0)))
122    {
123      $str .= ' ' if !($str eq "");
124      $str .= $tmp;
125      last if $tmp =~ /;\s*$/;
126      last if($block and $tmp =~ /\s*[}{]\s*$/);
127    }
128
129  $str =~ s/;$//;
130  return ($str);
131}
132
133
134sub
135add_dn_to_stack
136{
137  local ($dn) = @_;
138
139  $current_dn = "$dn, $current_dn";
140  $curentry{'current_dn'} = $current_dn;
141}
142
143
144sub
145remove_dn_from_stack
146{
147  $current_dn =~ s/^.*?,\s*//;
148}
149
150
151sub
152parse_error
153{
154  print "Parse error on line number $line_number at token number $token_number\n";
155  exit (1);
156}
157
158sub
159new_entry
160{
161   if (%curentry) {
162     $curentry{'current_dn'} = $current_dn;
163     push(@entrystack, {%curentry});
164     undef(%curentry);
165   }
166}
167
168sub
169pop_entry
170{
171  if (%curentry) {
172    push(@outputlist, {%curentry});
173  }
174  $rentry = pop(@entrystack);
175  %curentry = %$rentry if $rentry;
176}
177
178
179sub
180print_entry
181{
182  return if (scalar keys %curentry == 0);
183
184  if (!defined ($curentry{'type'}))
185    {
186      $hostdn = "cn=$server, $basedn";
187      print "dn: $hostdn\n";
188      print "cn: $server\n";
189      print "objectClass: top\n";
190      print "objectClass: dhcpServer\n";
191      print "dhcpServiceDN: $curentry{'current_dn'}\n";
192      if(grep(/FaIlOvEr/i, @use))
193        {
194          foreach my $fo_peer (keys %failover)
195            {
196              next if(scalar(@{$failover{$fo_peer}}) <= 1);
197              print "dhcpStatements: failover peer $fo_peer { ",
198                    join('; ', @{$failover{$fo_peer}}), "; }\n";
199            }
200        }
201      print "\n";
202
203      print "dn: $curentry{'current_dn'}\n";
204      print "cn: $dhcpcn\n";
205      print "objectClass: top\n";
206      print "objectClass: dhcpService\n";
207      if (defined ($curentry{'options'}))
208        {
209          print "objectClass: dhcpOptions\n";
210        }
211      print "dhcpPrimaryDN: $hostdn\n";
212      if(grep(/FaIlOvEr/i, @use) and ($second ne ''))
213        {
214          print "dhcpSecondaryDN: $second\n";
215        }
216    }
217  elsif ($curentry{'type'} eq 'subnet')
218    {
219      print "dn: $curentry{'current_dn'}\n";
220      print "cn: " . $curentry{'ip'} . "\n";
221      print "objectClass: top\n";
222      print "objectClass: dhcpSubnet\n";
223      if (defined ($curentry{'options'}))
224        {
225          print "objectClass: dhcpOptions\n";
226        }
227      
228      print "dhcpNetMask: " . $curentry{'netmask'} . "\n";
229      if (defined ($curentry{'ranges'}))
230        {
231          foreach $statement (@{$curentry{'ranges'}})
232            {
233              print "dhcpRange: $statement\n";
234            }
235        }
236    }
237  elsif ($curentry{'type'} eq 'shared-network')
238    {
239      print "dn: $curentry{'current_dn'}\n";
240      print "cn: " . $curentry{'descr'} . "\n";
241      print "objectClass: top\n";
242      print "objectClass: dhcpSharedNetwork\n";
243      if (defined ($curentry{'options'}))
244        {
245          print "objectClass: dhcpOptions\n";
246        }
247    }
248  elsif ($curentry{'type'} eq 'group')
249    {
250      print "dn: $curentry{'current_dn'}\n";
251      print "cn: group", $curentry{'idx'}, "\n";
252      print "objectClass: top\n";
253      print "objectClass: dhcpGroup\n";
254      if (defined ($curentry{'options'}))
255        {
256          print "objectClass: dhcpOptions\n";
257        }
258    }
259  elsif ($curentry{'type'} eq 'host')
260    {
261      print "dn: $curentry{'current_dn'}\n";
262      print "cn: " . $curentry{'host'} . "\n";
263      print "objectClass: top\n";
264      print "objectClass: dhcpHost\n";
265      if (defined ($curentry{'options'}))
266        {
267          print "objectClass: dhcpOptions\n";
268        }
269
270      if (defined ($curentry{'hwaddress'}))
271        {
272          $curentry{'hwaddress'} =~ y/[A-Z]/[a-z]/;
273          print "dhcpHWAddress: " . $curentry{'hwaddress'} . "\n";
274        }
275    }
276  elsif ($curentry{'type'} eq 'pool')
277    {
278      print "dn: $curentry{'current_dn'}\n";
279      print "cn: pool", $curentry{'idx'}, "\n";
280      print "objectClass: top\n";
281      print "objectClass: dhcpPool\n";
282      if (defined ($curentry{'options'}))
283        {
284          print "objectClass: dhcpOptions\n";
285        }
286
287      if (defined ($curentry{'ranges'}))
288        {
289          foreach $statement (@{$curentry{'ranges'}})
290            {
291              print "dhcpRange: $statement\n";
292            }
293        }
294    }
295  elsif ($curentry{'type'} eq 'class')
296    {
297      print "dn: $curentry{'current_dn'}\n";
298      print "cn: " . $curentry{'class'} . "\n";
299      print "objectClass: top\n";
300      print "objectClass: dhcpClass\n";
301      if (defined ($curentry{'options'}))
302        {
303          print "objectClass: dhcpOptions\n";
304        }
305    }
306  elsif ($curentry{'type'} eq 'subclass')
307    {
308      print "dn: $curentry{'current_dn'}\n";
309      print "cn: " . $curentry{'subclass'} . "\n";
310      print "objectClass: top\n";
311      print "objectClass: dhcpSubClass\n";
312      if (defined ($curentry{'options'}))
313        {
314          print "objectClass: dhcpOptions\n";
315        }
316      print "dhcpClassData: " . $curentry{'class'} . "\n";
317    }
318
319  if (defined ($curentry{'statements'}))
320    {
321      foreach $statement (@{$curentry{'statements'}})
322        {
323          print "dhcpStatements: $statement\n";
324        }
325    }
326
327  if (defined ($curentry{'options'}))
328    {
329      foreach $statement (@{$curentry{'options'}})
330        {
331          print "dhcpOption: $statement\n";
332        }
333    }
334
335  print "\n";
336  undef (%curentry);
337}
338
339
340sub parse_netmask
341{
342  local ($netmask) = @_;
343  local ($i);
344
345  if ((($a, $b, $c, $d) = $netmask =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) != 4)
346    {
347      parse_error ();
348    }
349
350  $num = (($a & 0xff) << 24) |
351         (($b & 0xff) << 16) |
352         (($c & 0xff) << 8) |
353          ($d & 0xff);
354
355  for ($i=1; $i<=32 && $num & (1 << (32 - $i)); $i++)
356    {
357    }
358  $i--;
359
360  return ($i);
361}
362
363
364sub parse_subnet
365{
366  local ($ip, $tmp, $netmask);
367
368  new_entry ();
369    
370  $ip = next_token (0);
371  parse_error () if !defined ($ip);
372
373  $tmp = next_token (1);
374  parse_error () if !defined ($tmp);
375  parse_error () if !($tmp eq 'netmask');
376
377  $tmp = next_token (0);
378  parse_error () if !defined ($tmp);
379  $netmask = parse_netmask ($tmp);
380
381  $tmp = next_token (0);
382  parse_error () if !defined ($tmp);
383  parse_error () if !($tmp eq '{');
384
385  add_dn_to_stack ("cn=$ip");
386  $curentry{'type'} = 'subnet';
387  $curentry{'ip'} = $ip;
388  $curentry{'netmask'} = $netmask;
389  $cursubnet = $ip;
390  $curcounter{$ip} = { pool  => 0, group => 0 };
391}
392
393
394sub parse_shared_network
395{
396  local ($descr, $tmp);
397
398  new_entry ();
399
400  $descr = next_token (0);
401  parse_error () if !defined ($descr);
402
403  $tmp = next_token (0);
404  parse_error () if !defined ($tmp);
405  parse_error () if !($tmp eq '{');
406
407  add_dn_to_stack ("cn=$descr");
408  $curentry{'type'} = 'shared-network';
409  $curentry{'descr'} = $descr;
410}
411
412
413sub parse_host
414{
415  local ($descr, $tmp);
416
417  new_entry ();
418
419  $host = next_token (0);
420  parse_error () if !defined ($host);
421
422  $tmp = next_token (0);
423  parse_error () if !defined ($tmp);
424  parse_error () if !($tmp eq '{');
425
426  add_dn_to_stack ("cn=$host");
427  $curentry{'type'} = 'host';
428  $curentry{'host'} = $host;
429}
430
431
432sub parse_group
433{
434  local ($descr, $tmp);
435
436  new_entry ();
437
438  $tmp = next_token (0);
439  parse_error () if !defined ($tmp);
440  parse_error () if !($tmp eq '{');
441
442  my $idx;
443  if(exists($curcounter{$cursubnet})) {
444    $idx = ++$curcounter{$cursubnet}->{'group'};
445  } else {
446    $idx = ++$curcounter{''}->{'group'};
447  }
448
449  add_dn_to_stack ("cn=group".$idx);
450  $curentry{'type'} = 'group';
451  $curentry{'idx'} = $idx;
452}
453
454
455sub parse_pool
456{
457  local ($descr, $tmp);
458
459  new_entry ();
460
461  $tmp = next_token (0);
462  parse_error () if !defined ($tmp);
463  parse_error () if !($tmp eq '{');
464
465  my $idx;
466  if(exists($curcounter{$cursubnet})) {
467    $idx = ++$curcounter{$cursubnet}->{'pool'};
468  } else {
469    $idx = ++$curcounter{''}->{'pool'};
470  }
471
472  add_dn_to_stack ("cn=pool".$idx);
473  $curentry{'type'} = 'pool';
474  $curentry{'idx'} = $idx;
475}
476
477
478sub parse_class
479{
480  local ($descr, $tmp);
481
482  new_entry ();
483
484  $class = next_token (0);
485  parse_error () if !defined ($class);
486
487  $tmp = next_token (0);
488  parse_error () if !defined ($tmp);
489  parse_error () if !($tmp eq '{');
490
491  $class =~ s/\"//g;
492  add_dn_to_stack ("cn=$class");
493  $curentry{'type'} = 'class';
494  $curentry{'class'} = $class;
495}
496
497
498sub parse_subclass
499{
500  local ($descr, $tmp);
501
502  new_entry ();
503
504  $class = next_token (0);
505  parse_error () if !defined ($class);
506
507  $subclass = next_token (0);
508  parse_error () if !defined ($subclass);
509
510  if (substr($subclass,-1) eq ';') {
511    $tmp = ";";
512    $subclass = substr($subclass,0,-1);
513  } else {
514    $tmp = next_token (0); 
515    parse_error () if !defined ($tmp);
516  }
517  parse_error () if !($tmp eq '{' or $tmp eq ';');
518  add_dn_to_stack ("cn=$subclass");
519  $curentry{'type'} = 'subclass';
520  $curentry{'class'} = $class;
521  $curentry{'subclass'} = $subclass;
522
523  if ($tmp eq ';') {
524    pop_entry ();
525    remove_dn_from_stack ();
526  }
527}
528
529
530sub parse_hwaddress
531{
532  local ($type, $hw, $tmp);
533
534  $type = next_token (1);
535  parse_error () if !defined ($type);
536
537  $hw = next_token (1);
538  parse_error () if !defined ($hw);
539  $hw =~ s/;$//;
540
541  $curentry{'hwaddress'} = "$type $hw";
542}
543
544    
545sub parse_range
546{
547  local ($tmp, $str);
548
549  $str = remaining_line ();
550
551  if (!($str eq ''))
552    {
553      $str =~ s/;$//;
554      push (@{$curentry{'ranges'}}, $str);
555    }
556}
557
558
559sub parse_statement
560{
561  local ($token) = shift;
562  local ($str);
563
564  if ($token eq 'option')
565    {
566      $str = remaining_line ();
567      push (@{$curentry{'options'}}, $str);
568    }
569  elsif($token eq 'failover')
570    {
571      $str = remaining_line (1); # take care on block
572      if($str =~ /[{]/)
573        {
574          my ($peername, @statements);
575
576          parse_error() if($str !~ /^\s*peer\s+(.+?)\s+[{]\s*$/);
577          parse_error() if(($peername = $1) !~ /^\"?[^\"]+\"?$/);
578
579          #
580          # failover config block found:
581          # e.g. 'failover peer "some-name" {'
582          #
583          if(not grep(/FaIlOvEr/i, @use))
584            {
585              print STDERR "Warning: Failover config 'peer $peername' found!\n";
586              print STDERR "         Skipping it, since failover disabled!\n";
587              print STDERR "         You may try out --use=failover option.\n";
588            }
589
590          until($str =~ /[}]/ or $str eq "")
591            {
592                $str = remaining_line (1);
593                # collect all statements, except ending '}'
594                push(@statements, $str) if($str !~ /[}]/);
595            }
596          $failover{$peername} = [@statements];
597        }
598      else
599        {
600          #
601          # pool reference to failover config is fine
602          # e.g. 'failover peer "some-name";'
603          #
604          if(not grep(/FaIlOvEr/i, @use))
605            {
606              print STDERR "Warning: Failover reference '$str' found!\n";
607              print STDERR "         Skipping it, since failover disabled!\n";
608              print STDERR "         You may try out --use=failover option.\n";
609            }
610          else
611            {
612              push (@{$curentry{'statements'}}, $token. " " . $str);
613            }
614        }
615    }
616  elsif($token eq 'zone')
617    {
618      $str = $token;
619      while($str !~ /}$/) {
620        $str .= ' ' . next_token (0);
621      }
622      push (@{$curentry{'statements'}}, $str);
623    }
624  elsif($token =~ /^(authoritative)[;]*$/)
625    {
626      push (@{$curentry{'statements'}}, $1);
627    }
628  else
629    {
630      $str = $token . " " . remaining_line ();
631      push (@{$curentry{'statements'}}, $str);
632    }
633}
634
635
636my $ok = GetOptions(
637    'basedn=s'      => \$basedn,
638    'dhcpdn=s'      => \$dhcpdn,
639    'server=s'      => \$server,
640    'second=s'      => \$second,
641    'conf=s'        => \$i_conf,
642    'ldif=s'        => \$o_ldif,
643    'use=s'         => \@use,
644    'h|help|usage'  => sub { usage(0); },
645);
646
647unless($server =~ /^\w+/)
648  {
649    usage(1, "invalid server name '$server'");
650  }
651unless($basedn =~ /^\w+=[^,]+/)
652  {
653    usage(1, "invalid base dn '$basedn'");
654  }
655
656if($dhcpdn =~ /^cn=([^,]+)/i)
657  {
658    $dhcpcn = "$1";
659  }
660$second = '' if not defined $second;
661unless($second eq '' or $second =~ /^cn=[^,]+\s*,\s*\w+=[^,]+/i)
662  {
663    if($second =~ /^cn=[^,]+$/i)
664      {
665        # relative DN 'cn=name'
666        $second = "$second, $basedn";
667      }
668    elsif($second =~ /^\w+/)
669      {
670        # assume hostname only
671        $second = "cn=$second, $basedn";
672      }
673    else
674      {
675        usage(1, "invalid secondary '$second'")
676      }
677  }
678
679usage(1) unless($ok);
680
681if($i_conf ne "" and -f $i_conf)
682  {
683    if(not open(STDIN, '<', $i_conf))
684      {
685        print STDERR "Error: can't open conf file '$i_conf': $!\n";
686        exit(1);
687      }
688  }
689if($o_ldif ne "")
690  {
691    if(-e $o_ldif)
692      {
693        print STDERR "Error: output ldif name '$o_ldif' already exists!\n";
694        exit(1);
695      }
696    if(not open(STDOUT, '>', $o_ldif))
697      {
698        print STDERR "Error: can't open ldif file '$o_ldif': $!\n";
699        exit(1);
700      }
701  }
702
703
704print STDERR "Creating LDAP Configuration with the following options:\n";
705print STDERR "\tBase DN: $basedn\n";
706print STDERR "\tDHCP DN: $dhcpdn\n";
707print STDERR "\tServer DN: cn=$server, $basedn\n";
708print STDERR "\tSecondary DN: $second\n"
709             if(grep(/FaIlOvEr/i, @use) and $second ne '');
710print STDERR "\n";
711
712my $token;
713my $token_number = 0;
714my $line_number = 0;
715my $cursubnet = '';
716my %curcounter = ( '' => { pool => 0, group => 0 } );
717
718$current_dn = "$dhcpdn";
719$curentry{'current_dn'} = $current_dn;
720$curentry{'descr'} = $dhcpcn;
721$line = '';
722%failover = ();
723
724while (($token = next_token (1)))
725  {
726    if ($token eq '}')
727      {
728        pop_entry ();
729        if($current_dn =~ /.+?,\s*${dhcpdn}$/) {
730          # don't go below dhcpdn ...
731          remove_dn_from_stack ();
732        }
733      }
734    elsif ($token eq 'subnet')
735      {
736        parse_subnet ();
737        next;
738      }
739    elsif ($token eq 'shared-network')
740      {
741        parse_shared_network ();
742        next;
743      }
744    elsif ($token eq 'class')
745      {
746        parse_class ();
747        next;
748      }
749    elsif ($token eq 'subclass')
750      {
751        parse_subclass ();
752        next;
753      }
754    elsif ($token eq 'pool')
755      {
756        parse_pool ();
757        next;
758      }
759    elsif ($token eq 'group')
760      {
761        parse_group ();
762        next;
763      }
764    elsif ($token eq 'host')
765      {
766        parse_host ();
767        next;
768      }
769    elsif ($token eq 'hardware')
770      {
771        parse_hwaddress ();
772        next;
773      }
774    elsif ($token eq 'range')
775      {
776        parse_range ();
777        next;
778      }
779    else
780      {
781        parse_statement ($token);
782        next;
783      }
784  }
785
786pop_entry ();
787
788while ($#outputlist >= 0) {
789  $rentry = pop(@outputlist);
790  if ($rentry) {
791    %curentry = %$rentry;
792    print_entry ();
793  }
794}
795
796close(STDIN)  if($i_conf);
797close(STDOUT) if($o_ldif);
798
799print STDERR "Done.\n";
800
801