Subject: [dhcwg] bind zones and dhcpd configuration from ldap

From: Yannick Majoros <[email-masked]>
Date: Tue, 15 Nov 2005 16:52:40 +0100

Hi,

  This is not a question, just for information. So, I hope not to 
disturb anyone by crossposting (sorry if I do)...

  I just wrote a script to create a configuration file and zone files 
from our openldap machine list, and restart the servers when 
appropriate. I thought it could be of interest for other people, so here 
it goes.

  I know other scripts exist, but I needed a different solution for the 
following reasons:

1. I didn't want to just write the dns records in our ldap base. 
Instead, these should be generated
2. I didn't want bind to just access the ldap base, because I want it to 
be independent of openldap and because of 1

  Instead, I wanted to just put all user workstations, servers, 
switchers and routers, and let a script generate the zone and dhcpd 
files. Please note that it is configurable: I use it myself to generate 
the files for multiple domains/subnets from the same base. Just change a 
couple of lines (I should put it in an independent config file when I'll 
have time).

  I wrote these scripts to suit my needs, but you should be able to 
adapt them to other infrastructures without too much hassle.

  A few details:

  - ldap_hosts is the main script. Just adjust the settings to your 
need. Some are optional; sorry for the poor doc, but the whole script is 
300 lines, so just have a look. 'NAMED' stuff is optional.
  - serials is a little script I wrote to update the serials of the zone 
files. Adjust your zones list in the script. It needs to access a file 
named 'serial' which contains your last serial (date/time format), and 
will update it itself.
  - I run these things in crontab, every minute. So, dns and dhcp are 
automatically updated whenever I change the ldap base, and I'm notified 
by mail about success/failures.
  - In case of failure, it generates temp files which are only 
deleted/changed when you change something. So, you won't receive a mail 
every minute for the same failure.
  - I use the script to generate only the parts of the zone/dhcpd files 
I need. I then use "include" directives in dhcpd.conf (include 
"/etc/dhcpd_ldap.conf";) and in the zone files ($INCLUDE machines.lst).
  - It works for me but could need some local adjustements!

  Ok, I think it's all your need. I hope it will be useful to someone 
someday, feel free to modify it (just tell me, please). Please give me 
your feedback.

  Yannick Majoros
#!/usr/bin/perl
use strict;
my $path="/var/named/";
my $ser_file="$path/serial";
my @args=$#ARGV==0?@ARGV:("auto.zone","inma.zone","csam.zone","239.zone");
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $ser=1+$mday*100+($mon+1)*10000+($year+2900)*1000000;
open(F,"<$ser_file") or die "Impossible d'ouvrir $ser_file";
my $oSer=<F>;
close(F);
my $cSer=$oSer>=$ser?$oSer+1:$ser;
die "SERIAL foireux : ancien=$oSer ; nouveau=$cSer" if $oSer-$cSer>=100;
open(F,">$ser_file") or die "Impossible d'ouvrir $ser_file";
print F $cSer;
close(F);
foreach my $arg (@args)
{
 my $file=$path.$arg;
 if (!-e $file)
 {
  die "$file non trouvé" if $file=~/\.zone$/;
  my $nfile=$file.".zone" if !-e $file;
  die "Ni $file ni $nfile n'ont pu être trouvés" if !-e $nfile;
  $file=$nfile;
 }
 open(ZONE,$file) or die "Échec à l'ouverture de $file";
 print "$file... ";
 my @lines=<ZONE>;
 close(ZONE);
 open(BAK,">$file.bak") or die "Échec à l'ouverture de >$file.bak";
 print BAK @lines;
 close(BAK);
 my $lines=join "",@lines;
 if ($lines=~/IN\s*?SOA.*\(\s*(\d*)/)
 {
  my $oSer=$1;
  print "ok ($oSer -> $cSer)\n";
  $lines=~s/IN\s*?SOA(.*\()\s*\d*/IN\tSOA$1\n\t\t\t$cSer/;
 }
 open(ZONE,">$file") or die "Échec à l'ouverture de >$file";
 print ZONE $lines;
 close(ZONE);
}
#!/usr/bin/perl
#######################################################################
# ldap_hosts: create dhcpd/bind files from ldap base
# Original author: Yannick Majoros
# Feel free to modify this, but please notify me if you found
# something interesting: [email-masked]
# Don't hesitate if you want to send me some money ;-)
# If you don't, you could at least send me a postcard:
#
#       Yannick Majoros
#       rue de Mellery, 36
#       1495 Villers-la-Ville
#       Belgium
#######################################################################
use strict;
use Net::LDAP;
use Digest::MD5;
use Text::Iconv;

#######################################################################
# your settings go below
#######################################################################
my $LDAP_HOST='ldap://yourserver.yourdomain';
my $LDAP_BASE='ou=Machines,dc=your,dc=domain';
my $LDAP_BINDDN='cn=Manager,dc=your,dc=domain';
my $LDAP_BINDPWD='';
my $LDAP_FILTER='(objectClass=device)';
my $DHCPD_FILE='/etc/dhcpd_ldap.conf';
my $DHCPD_COMMIT='/sbin/service dhcpd restart';
my $NAMED_RE='';
my $IP_FILTER="^130\.104\.239\.";
my $NAMED_FILE='/var/named/machines.lst';
my $NAMED_R_FILE='/var/named/reverse_239.lst';
my $NAMED_COMMIT='/var/named/serials && /usr/sbin/rndc reload && /sbin/service named restart';
my $REVERSE_DN='your.domain';
# convert from ldap base (utf-8) to local files
my $converter = Text::Iconv->new("utf-8","iso-8859-1");
#######################################################################

my $REPLACEFILE_UNCHANGED=0;
my $REPLACEFILE_CHANGED=1;
my $REPLACEFILE_COMMITED=2;

my $ldap=Net::LDAP->new($LDAP_HOST) or die "$@";
if ($LDAP_BINDDN ne ''&&$LDAP_BINDPWD ne '')
{
 my $mesg=$ldap->bind($LDAP_BINDDN,password=>$LDAP_BINDPWD);
 $mesg->code && die $mesg->error;
}
else
{
 my $mesg=$ldap->bind();
 $mesg->code && die $mesg->error;
}

my $command=shift @ARGV;
if ($command eq '-a')
{
 add(@ARGV);
}
elsif ($command eq '-l'||!$command)
{
 list();
}
else
{
 usage();
}
$ldap->unbind;

#######################################################################
# list hosts from ldap base (=create all files)
#######################################################################
sub list
{
 my %machines;
 my %ethernet;
 my @errors;
 my %zone;
 my %hosts;

 my $mesg=$ldap->search(base=>$LDAP_BASE,filter=>$LDAP_FILTER);
 $mesg->code && die $mesg->error;
 foreach my $entry ($mesg->entries)
 {
#print $entry->dn,"\n";
  my $cn=$converter->convert(''.$entry->get_value('cn'));
  if ($hosts{$cn})
  {
   push @errors,"Duplicate host: $cn";
  }
  $hosts{$cn}{'cn'}=$cn;
  $machines{$cn}=1;
  if ($entry->exists('description'))
  {
   my @descriptions=$entry->get_value('description');
   foreach my $description (@descriptions)
   {
    $description=$converter->convert($description);
    push @{$hosts{$cn}{'desc'}},$description;
   }
  }
  if ($entry->exists('macAddress'))
  {
   my @mac=$entry->get_value('macAddress');
   foreach my $mac (@mac)
   {
    push @{$hosts{$cn}{'mac'}},$mac;
    if ($ethernet{$mac}==1)
    {
     push @errors,"Duplicate mac: $mac ($cn)";
    }
    $ethernet{$mac}=1;
   }
  }
  if ($entry->exists('ipHostNumber'))
  {
   my @ips=$entry->get_value('ipHostNumber');
   foreach my $ip (@ips)
   {
    $zone{$ip}=$cn if $ip=~$IP_FILTER;
    push @{$hosts{$cn}{'ip'}},$ip;
   }
  }
  if ($entry->exists('bootFile'))
  {
   my $bootfile=$converter->convert(''.$entry->get_value('bootFile')); 
   push @{$hosts{$cn}{'bootfile'}},$bootfile;
  }
 }

# CREATING DHCPD FILE

 my $dhcpd_data;
 my @sorted_hosts=sort {cmp_hosts($hosts{$a},$hosts{$b})} keys %hosts;
 foreach my $host (@sorted_hosts)
 {
  foreach my $desc (@{$hosts{$host}{'desc'}})
  {
   $dhcpd_data.="# $desc\n";
  }
  if (!$hosts{$host}{'mac'})
  {
   $dhcpd_data.="# NO MAC FOR THIS HOST!";
   next;
  }
  $dhcpd_data.="host $host {\n"; 
  foreach my $mac (@{$hosts{$host}{'mac'}})
  {
   $dhcpd_data.="\thardware ethernet $mac;\n";
  }
  my $last_ip='';
  foreach my $ip (@{$hosts{$host}{'ip'}})
  {
   $last_ip=$ip if $ip=~$IP_FILTER;
  }
  if ($last_ip)
  {
   $dhcpd_data.="\tfixed-address $last_ip;\n";
  }
  foreach my $bootfile (@{$hosts{$host}{'bootfile'}})
  {
   $dhcpd_data.= "\tfilename \"$bootfile\";\n";
  }
  $dhcpd_data.="}\n\n";
 }
 my $err=@errors;
# CREATING NAMED FILE
 if ($NAMED_FILE)
 {
  my $named_data;
  my $named_r_data;
#my @sorted_ips=sort { sprintf("%d%03d%03d%03d",split /\./,$a)<=>sprintf("%d%03d%03d%03d",split /\./,$b) } keys %zone;
  my @sorted_ips=sort { cmp_ip($a,$b) } keys %zone;
  foreach my $ip (@sorted_ips)
  {
   my $print_host=my $host=$zone{$ip};
   $print_host.=" "x(16-length $host) if length $host<16;
   $named_data.="$print_host\t\tIN\tA\t$ip\n";
   my $ip_last=$ip;
   $ip_last=~s/^.*\.//;
   $named_r_data.="$ip_last\t86400\tIN\tPTR\t$host.$REVERSE_DN.\n";
   foreach my $desc (@{$hosts{$host}{'desc'}})
   {
    $named_data.="\t\t\t\tTXT \"$desc\"\n";
   }
  } 
# WRITING CHANGES
  replace_file($NAMED_R_FILE,$named_r_data,'',$err) if $NAMED_R_FILE;
  replace_file($NAMED_FILE,$named_data,$NAMED_COMMIT,$err);
 }
 my $changed=replace_file($DHCPD_FILE,$dhcpd_data,$DHCPD_COMMIT,$err);
 if ($changed&&@errors)
 {
  print "\nErrors: \n *",(join "\n *",@errors),"\n";
 }
}
#######################################################################
# show usage info
#######################################################################
sub usage
{
 die << "__EOF__";
USAGE:
 ldap2dhcp.pl [-l]
 ldap2dhcp.pl -a <MACHINE_NAME> <MAC_ADDRESS>
__EOF__
}
#######################################################################
# add a host to ldap base - unimplemented
#######################################################################
sub add
{
 my ($name,$mac,$manager)=@_;
# UNIMPLEMENTED :-)
 die "Command unimplemented.\n";
}
#######################################################################
# replace a file if MD5 sums differ 
# (this should be the case when they differ
#######################################################################
sub replace_file
{
 my ($filename,$data,$commit,$errors)=@_;
 my $tmp_file="$filename.tmp";
 my $input_file=-e "$tmp_file"?$tmp_file:$filename;
 my $ctx1=Digest::MD5->new;
 my $ctx2=Digest::MD5->new;
 if (-e $input_file)
 {
  open(CHECK,"<$input_file") or die "Cannot open $input_file!\n";
  $ctx1->addfile(*CHECK);
  close(CHECK);
 }
 $ctx2->add($data);
 my $hex1=$ctx1->hexdigest;
 my $hex2=$ctx2->hexdigest;
 if ($hex1 ne $hex2)
 {
  # destination file and data differ
  print "Machine list changed in LDAP...\n";
  if ($errors)
  {
   print "No commit to $filename, because there where some errors.\n";
   open(OUTF,">$tmp_file");
   print OUTF $data;
   close OUTF;
   return $REPLACEFILE_CHANGED;
  }
  else
  {
   print "Writing $filename...\n";
   open(OUTF,">$filename");
   print OUTF $data;
   close OUTF;
   unlink($tmp_file) if (-e $tmp_file);
   if ($commit)
   {
    print "Commiting changes with command: $commit\n"; 
    system($commit);
    print "\n";
   }
   return $REPLACEFILE_COMMITED;
  }
 }
 return $REPLACEFILE_UNCHANGED;
}
#######################################################################
# cmp-like operator to compare two ip addresses
# could be more effecient, but this one is readable
#######################################################################
sub cmp_ip
{
 my ($a,$b)=@_;
 return sprintf("%d%03d%03d%03d",split /\./,$a)<=>sprintf("%d%03d%03d%03d",split /\./,$b);
}
#######################################################################
# return highest ip of two
#######################################################################
sub greatest_ip
{
 my (@ip_list)=@_;
 my $res;
 foreach my $ip (@ip_list)
 {
  if ($ip=~$IP_FILTER)
  {
   $res=(cmp_ip($ip,$res))?$ip:$res;
  }
 }
 return $res; 
}
#######################################################################
# cmp-like function to sort two hosts based on ip or name
#######################################################################
sub cmp_hosts
{
 my ($a,$b)=@_;
 my %host_a=%{$a};
 my %host_b=%{$b};
 if (!$host_a{'ip'}&&!$host_b{'ip'})
 {
  return ($host_a{'cn'} cmp $host_b{'cn'});
 }
 my $ip_a=greatest_ip(@{$host_a{'ip'}});
 my $ip_b=greatest_ip(@{$host_b{'ip'}});
 return cmp_ip($ip_a,$ip_b);
}
_______________________________________________
dhcwg mailing list
[email-masked]
https://www1.ietf.org/mailman/listinfo/dhcwg

---

From: Ted Lemon <[email-masked]>
Date: Tue, 15 Nov 2005 10:43:12 -0700

> disturb anyone by crossposting (sorry if I do)...

So does this mean that you're going to write an LDAP schema for DHCP?   :')

It's something that comes up from time to time.   If you are interested, let 
us know!

---

From: Stig Venaas <[email-masked]>
Date: Sun, 27 Nov 2005 13:43:04 +0100

> It's something that comes up from time to time.   If you are interested, let 

I was considering doing this years ago (back when I was involved with
other LDAP stuff). The main problem I found, was not the schema itself,
but rather figuring out exactly what information needs to be stored,
and doing it in a way that is generic enough to work with most servers.

Doing it for one particular server may not be too hard, and I believe
some schema and code already exists for the ISC server.

Stig