#!/usr/bin/perl -wT # # EMULAB-COPYRIGHT # Copyright (c) 2000-2006 University of Utah and the Flux Group. # All rights reserved. # use English; use Getopt::Std; use Fcntl ':flock'; # # dhcpd_makeconf - helper script to create dhcpd.conf files from the database. # The template file should look like an ordinary dhcpd.conf file, but have # the string %%nodetype= where you want entries for a set of nodes # filled out. See the template file in the dhcp directory for an example. # sub usage { print "Usage: $0 [-h] [-v] [-i] [-t ]\n"; print "-h Show this message\n"; print "-v Use virtual names, when possible, for hostnames\n"; print "-i Install new config file to standard location.\n"; print "-r Restart DHCPD after config file regeneration (with -i only)\n"; print "Without -i, spits out a dhcpd config file on stdout, as in:\n"; print " $0 > dhcpd.conf\n"; exit(1); } my $optlist = "ihvt:r"; my $install = 0; my $vnames = 0; my $restart = 0; # # Configure variables # my $TBOPS = "@TBOPSEMAIL@"; # un-taint path $ENV{'PATH'} = '/bin:/usr/bin:/usr/sbin:/usr/local/bin'; delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; # Turn off line buffering on output $| = 1; use lib "@prefix@/lib"; use libdb; use libtestbed; my $CRTLTAG = TBDB_IFACEROLE_CONTROL(); my $DHCPD_CONF = "/usr/local/etc/dhcpd.conf"; my $DHCPD_TEMPLATE = "/usr/local/etc/dhcpd.conf.template"; my $lockfile = "/var/tmp/testbed_dhcpd_lockfile"; my %servernodes = (); my $template = $DHCPD_TEMPLATE; my $outfile = "/tmp/dhcpd_makeconf.$$"; my $OUT = *STDOUT; # # Parse command arguments. Once we return from getopts, all that should # left are the required arguments. # %options = (); if (! getopts($optlist, \%options)) { usage(); } if (defined($options{"i"})) { $install = 1; # We don't want to run this script unless its the real version. if ($EUID != 0) { die("*** $0:\n". " Must be root! Maybe its a development version?\n"); } if (defined($options{"r"})) { $restart = 1; } } if (defined($options{"h"})) { usage(); } if (defined($options{"v"})) { $vnames = 1; } if (defined($options{"t"})) { $template = $options{"t"}; # # Untaint argument; Allow slash. # if ($template =~ /^([-\w\.\/]+)$/) { $template = $1; } else { die("Tainted template name: $template\n"); } } # # If we are going to actually install this file, must serialize to # avoid a trashed config file. Use a dummy file in /var/tmp, opened for # writing and flock'ed. # if ($install) { open(LOCK, ">>$lockfile") || fatal("Couldn't open $lockfile\n"); my $count = 0; if (flock(LOCK, LOCK_EX|LOCK_NB) == 0) { # # If we don't get it the first time, we wait for: # 1) The lock to become free, in which case we do our thing # 2) The time on the lock to change, in which case we wait for # that process to finish # my $oldlocktime = (stat(LOCK))[9]; my $gotlock = 0; while (1) { print "Another dhcpd config update in progress, ". "waiting for it to finish\n"; if (flock(LOCK, LOCK_EX|LOCK_NB) != 0) { # OK, got the lock, we can do what we're supposed to $gotlock = 1; last; } my $locktime = (stat(LOCK))[9]; if ($locktime != $oldlocktime) { $oldlocktime = $locktime; last; } if ($count++ > 30) { fatal("Could not get the lock after a long time!\n"); } sleep(1); } $count = 0; # # If we didn't get the lock, wait for the processes that did to finish # if (!$gotlock) { while (1) { if ((stat(LOCK))[9] != $oldlocktime) { exit(0); } if (flock(LOCK, LOCK_EX|LOCK_NB) != 0) { close(LOCK); exit(0); } if ($count++ > 30) { fatal("Process with the lock didn't finish after a ". "long time!\n"); } sleep(1); } } } # # Perl-style touch(1) # my $now = time; utime $now, $now, $lockfile; # # Open temporary output file. # open(OF, ">$outfile") or fatal("Could not open $outfile\n"); $OUT = *OF; } open(IF,"<$template") or fatal("Unable to open $template for reading"); while () { if (/^(\s*)\%\%nodetype=(\w+)/) { my $spaces = $1; my $nodetype = $2; my $query_result = DBQueryWarn("select n.node_id,n.pxe_boot_path, ". " t.pxe_boot_path as nt_pxe_boot_path, ". " i.IP,i.MAC,r.pid,r.eid,r.vname, ". " r.inner_elab_role,r.inner_elab_boot, ". " r.plab_role,r.plab_boot ". "from nodes as n ". "left join interfaces as i on ". " n.node_id=i.node_id ". "left join node_types as t on n.type=t.type ". "left join reserved as r on ". " n.node_id=r.node_id ". "where (n.type='$nodetype' or ". " t.class='$nodetype') and ". " i.role='$CRTLTAG' ". "order BY n.priority"); if (! $query_result) { fatal("Could not dhcpd data from DB!"); } # # First go through and find any server (boss or plc) nodes. # We need these to create the next-server line for elabinelab # and plabinelab nodes. # while (my %row = $query_result->fetchhash()) { if (defined($row{"pid"}) && ((defined($row{"inner_elab_role"}) && ($row{"inner_elab_role"} eq "boss" || $row{"inner_elab_role"} eq "boss+router")) || (defined($row{"plab_role"}) && $row{"plab_role"} eq "plc"))) { my $tag = $row{"pid"} . ":" . $row{"eid"}; $servernodes{$tag} = $row{"IP"}; } } $query_result->dataseek(0); while (my %row = $query_result->fetchhash()) { my $ip = $row{"IP"}; my $mac = $row{"MAC"}; my $node_id; my $next_server = ""; my $hostname = ""; my $dns = ""; my $filename = ""; if ($vnames && defined($row{"vname"})) { $node_id = $row{"vname"}; } else { $node_id = $row{"node_id"}; } if (defined($row{"pid"}) && (($row{"inner_elab_boot"} == 1 && defined($row{"inner_elab_role"}) && $row{"inner_elab_role"} eq "node") || ($row{"plab_boot"} == 1 && defined($row{"plab_role"}) && $row{"plab_role"} eq "node"))) { my $tag = $row{"pid"} . ":" . $row{"eid"}; $next_server = "${spaces}\tnext-server " . $servernodes{$tag} . ";\n"; if ($row{"inner_elab_boot"} == 1) { $dns = "${spaces}\toption domain-name-servers ". "1.1.1.1;\n"; } } else { $hostname = "${spaces}\toption host-name \"$node_id\";\n"; } # # Handle alternate boot program filename if it exists. # Use mutable nodes.pxe_boot_path if it is defined. # Otherwise use the node_types.pxe_boot_path if it is # defined. Otherwise don't set anything (use the global # default). # if (defined($row{"pxe_boot_path"})) { my $fn = $row{"pxe_boot_path"}; # make sure it is pretty constrained if ($fn =~ /^\/tftpboot\// && $fn !~ /\.\./) { $filename = "${spaces}\tfilename \"$fn\";\n"; } } if (!$filename && defined($row{"nt_pxe_boot_path"})) { my $fn = $row{"nt_pxe_boot_path"}; # make sure it is pretty constrained if ($fn =~ /^\/tftpboot\// && $fn !~ /\.\./) { $filename = "${spaces}\tfilename \"$fn\";\n"; } } # Need to make MAC look right.. $mac =~ s/(..)\B/$1:/g; print $OUT "${spaces}host $ip {\n"; print $OUT $filename; print $OUT $next_server; print $OUT $dns; print $OUT "${spaces}\thardware ethernet $mac;\n"; print $OUT $hostname; print $OUT "${spaces}\tfixed-address $ip;\n"; print $OUT "${spaces}}\n\n"; } } else { # It's a regular line print $OUT $_; } } close(IF); if ($install) { close(OF) or fatal("Could not close $outfile"); if (-e $DHCPD_CONF) { system("cp -fp $DHCPD_CONF ${DHCPD_CONF}.old") == 0 or fatal("Could not backup copy of ${DHCPD_CONF}"); } system("mv -f $outfile $DHCPD_CONF") == 0 or fatal("Could not install new ${DHCPD_CONF}"); if ($restart) { $dpid = `cat /var/run/dhcpd.pid`; chomp($dpid); # untaint if ($dpid =~ /^([\d]+)$/) { $dpid = $1; } else { fatal("Bad pid for DHCPD: $dpid"); } if (kill('TERM', $dpid) == 0) { fatal("Could not kill(TERM) process $dpid (dhcpd): $!"); } } } exit(0); # # Die. # sub fatal { my $msg = $_[0]; die($msg); }