Home | History | Annotate | Line # | Download | only in ldap
      1 #!/usr/bin/perl -w
      2 
      3 # Brian Masney <masneyb (at] 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 
     25 use Net::Domain qw(hostname hostfqdn hostdomain);
     26 use Getopt::Long;
     27 
     28 my $domain = hostdomain();           # your.domain
     29 my $basedn = "dc=".$domain;
     30    $basedn =~ s/\./,dc=/g;           # dc=your,dc=domain
     31 my $server = hostname();             # hostname (nodename)
     32 my $dhcpcn = 'DHCP Config';          # CN of DHCP config tree
     33 my $dhcpdn = "cn=$dhcpcn, $basedn";  # DHCP config tree DN
     34 my $second = '';                     # secondary server DN / hostname
     35 my $i_conf = '';                     # dhcp.conf file to read or stdin
     36 my $o_ldif = '';                     # output ldif file name or stdout
     37 my @use    = ();                     # extended flags (failover)
     38 
     39 sub 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__;
     46 usage: 
     47   $0 [options] < dhcpd.conf > dhcpd.ldif
     48 
     49 options:
     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 
     68 sub 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 
    115 sub 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 
    134 sub
    135 add_dn_to_stack
    136 {
    137   local ($dn) = @_;
    138 
    139   $current_dn = "$dn, $current_dn";
    140   $curentry{'current_dn'} = $current_dn;
    141 }
    142 
    143 
    144 sub
    145 remove_dn_from_stack
    146 {
    147   $current_dn =~ s/^.*?,\s*//;
    148 }
    149 
    150 
    151 sub
    152 parse_error
    153 {
    154   print "Parse error on line number $line_number at token number $token_number\n";
    155   exit (1);
    156 }
    157 
    158 sub
    159 new_entry
    160 {
    161    if (%curentry) {
    162      $curentry{'current_dn'} = $current_dn;
    163      push(@entrystack, {%curentry});
    164      undef(%curentry);
    165    }
    166 }
    167 
    168 sub
    169 pop_entry
    170 {
    171   if (%curentry) {
    172     push(@outputlist, {%curentry});
    173   }
    174   $rentry = pop(@entrystack);
    175   %curentry = %$rentry if $rentry;
    176 }
    177 
    178 
    179 sub
    180 print_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 
    340 sub 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 
    364 sub 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 
    394 sub 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 
    413 sub 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 
    432 sub 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 
    455 sub 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 
    478 sub 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 
    498 sub 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 
    530 sub 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     
    545 sub 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 
    559 sub 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 
    636 my $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 
    647 unless($server =~ /^\w+/)
    648   {
    649     usage(1, "invalid server name '$server'");
    650   }
    651 unless($basedn =~ /^\w+=[^,]+/)
    652   {
    653     usage(1, "invalid base dn '$basedn'");
    654   }
    655 
    656 if($dhcpdn =~ /^cn=([^,]+)/i)
    657   {
    658     $dhcpcn = "$1";
    659   }
    660 $second = '' if not defined $second;
    661 unless($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 
    679 usage(1) unless($ok);
    680 
    681 if($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   }
    689 if($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 
    704 print STDERR "Creating LDAP Configuration with the following options:\n";
    705 print STDERR "\tBase DN: $basedn\n";
    706 print STDERR "\tDHCP DN: $dhcpdn\n";
    707 print STDERR "\tServer DN: cn=$server, $basedn\n";
    708 print STDERR "\tSecondary DN: $second\n"
    709              if(grep(/FaIlOvEr/i, @use) and $second ne '');
    710 print STDERR "\n";
    711 
    712 my $token;
    713 my $token_number = 0;
    714 my $line_number = 0;
    715 my $cursubnet = '';
    716 my %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 
    724 while (($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 
    786 pop_entry ();
    787 
    788 while ($#outputlist >= 0) {
    789   $rentry = pop(@outputlist);
    790   if ($rentry) {
    791     %curentry = %$rentry;
    792     print_entry ();
    793   }
    794 }
    795 
    796 close(STDIN)  if($i_conf);
    797 close(STDOUT) if($o_ldif);
    798 
    799 print STDERR "Done.\n";
    800 
    801