dhcpd_makeconf.in 10.7 KB
Newer Older
1
#!/usr/bin/perl -wT
Leigh B. Stoller's avatar
Leigh B. Stoller committed
2 3
#
# EMULAB-COPYRIGHT
4
# Copyright (c) 2000-2006 University of Utah and the Flux Group.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
5 6
# All rights reserved.
#
7
use English;
8
use Getopt::Std;
9
use Fcntl ':flock';
10

11 12 13 14
#
# 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=<type> where you want entries for a set of nodes
15
# filled out. See the template file in the dhcp directory for an example.
16
#
17 18 19 20 21
sub usage {
    print "Usage: $0 [-h] [-v] [-i] [-t <templatefile>]\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";
22
    print "-r 	Restart DHCPD after config file regeneration (with -i only)\n";
23 24 25 26
    print "Without -i, spits out a dhcpd config file on stdout, as in:\n";
    print "  $0 > dhcpd.conf\n";
    exit(1);
}
27
my $optlist = "ihvt:r";
28 29
my $install = 0;
my $vnames  = 0;
30
my $restart = 0;
31

32
#
33
# Configure variables
34
#
35 36 37 38 39 40 41 42 43
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;

44
use lib "@prefix@/lib";
45 46
use libdb;
use libtestbed;
47
use NodeType;
48 49 50 51

my $CRTLTAG	    = TBDB_IFACEROLE_CONTROL();
my $DHCPD_CONF	    = "/usr/local/etc/dhcpd.conf";
my $DHCPD_TEMPLATE  = "/usr/local/etc/dhcpd.conf.template";
52
my %servernodes	    = ();
53 54 55
my %dhcp_subbosses  = ();
my %tftp_subbosses  = ();
my %bootinfo_subbosses  = ();
56
my %singlectlnet    = ();
57 58
my $template        = $DHCPD_TEMPLATE;
my $outfile	    = "/tmp/dhcpd_makeconf.$$";
59
my $OUT		    = *STDOUT;
60 61 62
my %subboss_dhcp_servers;

sub RestartSubbossDhcp($$);
63

64
#
65 66
# Parse command arguments. Once we return from getopts, all that should
# left are the required arguments.
67
#
68 69 70 71 72 73
%options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"i"})) {
    $install = 1;
74

75 76 77 78 79
    # 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");
    }
80 81 82
    if (defined($options{"r"})) {
	$restart = 1;
    }
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
}
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");
    }
}
103

104
#
105
# If we are going to actually install this file, must serialize to
106
# avoid a trashed config file.
107
#
108
if ($install) {
109 110 111 112
    if ((my $locked = TBScriptLock("dhcpd.conf", 1)) != TBSCRIPTLOCK_OKAY()) {
	exit(0)
	    if ($locked == TBSCRIPTLOCK_IGNORE);
	fatal("Could not get the lock after a long time!\n");
113
    }
114

115 116
    #
    # Open temporary output file.
117
    #
118 119 120 121
    open(OF, ">$outfile") or
	fatal("Could not open $outfile\n");
    $OUT = *OF;
}
122

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
#
# Locate all the "alternate server" nodes (inner elab boss nodes or
# inner plab PLC nodes).  We do this now rather than in the per-nodetype
# query below because the server may not be of the same type as the nodes
# they serve!
#
my $query_result =
    DBQueryWarn("select r.node_id,i.IP,r.pid,r.eid ".
		"from reserved as r ".
                "left join interfaces as i on ".
                "     r.node_id=i.node_id ".
                "where (r.inner_elab_role='boss' or ".
		"       r.inner_elab_role='boss+router' or ".
		"       r.plab_role='plc') and ".
		"      i.role='$CRTLTAG' ");
if (! $query_result) {
    fatal("Could not dhcpd data from DB!");
}
while (my %row = $query_result->fetchhash()) {
    if (defined($row{"pid"}) && defined($row{"eid"})) {
143 144 145
	my $pid = $row{"pid"};
	my $eid = $row{"eid"};
	my $tag = "${pid}:${eid}";
146

147
	$servernodes{$tag} = $row{"IP"};
148 149 150 151 152 153 154 155 156 157 158 159 160

	#
	# Need to know if this is a single or dual control network model.
	#
	my $exp_result =
	    DBQueryFatal("select elabinelab_singlenet from experiments ".
			 "where pid='$pid' and eid='$eid'");
	if ($exp_result->num_rows) {
	    $singlectlnet{$tag} = ($exp_result->fetchrow_array())[0];
	}
	else {
	    $singlectlnet{$tag} = 0;
	}
161 162 163
    }
}

164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
# Slurp in all of the subboss data to know A) if we should serve the DHCP
# lease to a given node, or if the next-server field should be something
# other than the IP for boss.
$query_result =
    DBQueryWarn("select s.node_id, s.service, s.subboss_id, i.IP ".
		"from subbosses as s left join interfaces as i on ".
		"s.subboss_id=i.node_id where i.role='$CRTLTAG'");
if (! $query_result) {
    fatal("Could not dhcpd data from DB!");
}
while (my %row = $query_result->fetchhash()) {
    my $node_id = $row{"node_id"};

    if ($row{"service"} eq 'tftp') {
	    $tftp_subbosses{$node_id} = $row{"IP"};
    } elsif ($row{"service"} eq 'dhcp') {
	    $dhcp_subbosses{$node_id} = $row{"IP"};
181
	    $subboss_dhcp_servers{$row{"subboss_id"}} = 1;
182 183 184 185 186
    } elsif ($row{"service"} eq 'bootinfo') {
	    $bootinfo_subbosses{$node_id} = $row{"IP"};
    }
}

187 188
open(IF,"<$template") or
    fatal("Unable to open $template for reading");
189
while (<IF>) {
190 191 192 193 194
	if (/^(\s*)range\s*;/) {
		# Comment out a null DHCPD_DYNRANGE line.
		my $spaces = $1;
		print $OUT "${spaces}#range ... ...;\n";
	} elsif (/^(\s*)\%\%nodetype=(\w+)/) {
195 196
		my $spaces = $1;
		my $nodetype = $2;
197 198

		$query_result =
199
		    DBQueryWarn("select n.node_id,n.pxe_boot_path,n.type, ".
200 201 202
				"       i.IP,i.MAC,r.pid,r.eid,r.vname, ".
				"       r.inner_elab_role,r.inner_elab_boot, ".
				"       r.plab_role,r.plab_boot ".
203 204 205 206 207 208 209 210 211 212 213 214
				"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!");
215
		}
216 217 218 219 220 221

		while (my %row = $query_result->fetchhash()) {
		    my $ip  = $row{"IP"};
		    my $mac = $row{"MAC"};
		    my $node_id;
		    my $next_server = "";
222
		    my $bootinfo_server = "";
223
		    my $hostname = "";
224
		    my $dns = "";
225
		    my $booting = "";
226
		    my $filename = "";
227 228 229 230 231 232 233 234

		    if ($vnames && defined($row{"vname"})) {
			$node_id = $row{"vname"};
		    }
		    else {
			$node_id = $row{"node_id"};
		    }

235 236 237 238 239 240
		    my $tag;
		    if (defined($row{"pid"})) {
			$tag = $row{"pid"} . ":" . $row{"eid"};
		    }

		    if (defined($tag) && defined($servernodes{$tag}) &&
241 242 243 244 245 246
			(($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"))) {
247

Mike Hibler's avatar
Mike Hibler committed
248 249 250 251 252 253 254 255 256 257
			#
			# XXX it is not yet completely clear what all we
			# need to do, or not do, for plab nodes.
			#
			if ($row{"plab_boot"} == 1) {
			    $hostname =
				"${spaces}\toption host-name \"$node_id\";\n";
			    $next_server = "${spaces}\tnext-server " .
				$servernodes{$tag} . ";\n";
			} else {
258 259 260 261 262 263 264 265 266 267 268
			    if ($singlectlnet{$tag}) {
				$hostname = "${spaces}\toption host-name ".
				    "\"$node_id\";\n";
				$booting  = "${spaces}\tignore booting;\n";
			    }
			    else {
				$next_server = "${spaces}\tnext-server " .
				    $servernodes{$tag} . ";\n";
				$dns = "${spaces}\toption ".
				    "domain-name-servers 1.1.1.1;\n";
			    }
269
			}
270
		    }
271 272 273 274 275
		    elsif (defined $dhcp_subbosses{$node_id}) {
			$hostname =
			    "${spaces}\toption host-name \"$node_id\";\n";
	                $booting = "${spaces}\tignore booting;\n";
		    }
276 277 278
		    else {
			$hostname =
			    "${spaces}\toption host-name \"$node_id\";\n";
279

280 281 282 283
		        if (defined $tftp_subbosses{$node_id}) {
			    $next_server = "${spaces}\tnext-server " .
			        $tftp_subbosses{$node_id} . ";\n";
		        }
284

285 286 287 288
		        if (defined $bootinfo_subbosses{$node_id}) {
			    $bootinfo_server = "${spaces}\toption PXE.emulab-bootinfo " .
			        $bootinfo_subbosses{$node_id} . ";\n";
		        }
289

290
		    }
291

292 293 294 295 296 297 298
		    #
		    # 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).
		    #
299 300 301 302 303 304 305
		    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";
			}
		    }
306 307 308 309 310 311 312 313 314
		    if (!$filename) {
			# Get the type info for this type.
			my $nodetype = NodeType->Lookup($row{"type"});
			# Get the type specific version of pxe_boot_path
			my $nt_pxe_boot_path;

			$nodetype->pxe_boot_path(\$nt_pxe_boot_path) == 0
                            or fatal("Could not get pxe_boot_path for ".
				     "$node_id");
315

316 317
			if (defined($nt_pxe_boot_path)) {
			    my $fn = $nt_pxe_boot_path;
318

319 320 321 322
			    # make sure it is pretty constrained
			    if ($fn =~ /^\/tftpboot\// && $fn !~ /\.\./) {
				$filename = "${spaces}\tfilename \"$fn\";\n";
			    }
323 324
			}
		    }
325

326 327 328 329
		    # Need to make MAC look right..
		    $mac =~ s/(..)\B/$1:/g;

		    print $OUT "${spaces}host $ip {\n";
330
		    print $OUT $filename;
331
		    print $OUT $next_server;
332
		    print $OUT $bootinfo_server;
333
		    print $OUT $dns;
334
		    print $OUT $booting;
335 336 337 338
		    print $OUT "${spaces}\thardware ethernet $mac;\n";
		    print $OUT $hostname;
		    print $OUT "${spaces}\tfixed-address $ip;\n";
		    print $OUT "${spaces}}\n\n";
339 340
		}
	} else {
341 342
	    # It's a regular line
	    print $OUT $_;
343 344
	}
}
345
close(IF);
346 347 348 349 350 351 352 353 354 355 356

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}");
357 358 359 360 361 362 363 364 365 366 367 368 369 370

    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): $!");
	}
371

372 373
        my $old_uid = $UID;
        $UID = $EUID;
374 375 376
        for my $subboss (keys %subboss_dhcp_servers) {
            RestartSubbossDhcp($subboss, $restart);
        }
377
        $UID = $old_uid;
378 379
    }

380
    TBScriptUnlock();
381
}
382
exit(0);
383

384 385 386 387 388
#
# Die.
#
sub fatal {
    my $msg = $_[0];
389

390 391
    TBScriptUnlock()
	if ($install);
392

393 394
    die("*** $0:\n".
	"    $msg\n");
395
}
396

397 398 399
sub RestartSubbossDhcp($$)
{
        my ($subboss, $restart) = @_;
400
        
401
        my $cmd = "ssh $subboss @prefix@/sbin/subboss_dhcpd_makeconf";
402 403 404 405 406 407 408 409 410

        $cmd .= " -r" if ($restart);

        print "Restarting dhcpd on subboss $subboss\n";
        if (system($cmd)) {
	    print STDERR "Failed to restart dhcpd on $subboss\n";
	}
}