named_setup.in 16.9 KB
Newer Older
1
#!/usr/bin/perl -w
Leigh B. Stoller's avatar
Leigh B. Stoller committed
2
#
3
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
23
#
24
use English;
25
use Socket;
26
use strict;
27 28 29 30

#
# Suck out virtual names and create CNAME map entries.
#
31 32
# This script always does the right thing, so it does not matter who calls it. 
#
33
# usage: named_setup [-norestart]
34 35
#

36 37 38 39 40 41 42
#
# Function phototypes
#

sub assemble_zonefile($);
sub make_forward_zonefile($$$);
sub isroutable($);
43
sub IsJailIP($);
44
sub process_nodes($);
45

46 47 48 49
#
# Configure variables
#
my $TB		= "@prefix@";
50
my $TBOPS       = "@TBOPSEMAIL@";
51
my $USERS	= "@USERNODE@";
52
my $DISABLED    = "@DISABLE_NAMED_SETUP@";
53
my $OURDOMAIN   = "@OURDOMAIN@";
54 55
my $VIRTNODE_NETWORK   = "@VIRTNODE_NETWORK@";
my $VIRTNODE_NETMASK   = "@VIRTNODE_NETMASK@";
56

Leigh B. Stoller's avatar
Leigh B. Stoller committed
57
my $mapdir			= "/etc/namedb";
58
my $mapfile			= "${mapdir}/${OURDOMAIN}.db";
59 60 61
my $mapfiletail			= "$mapfile.tail";
my $mapfile_internal		= "$mapdir/${OURDOMAIN}.internal.db";
my $mapfile_internal_head	= "$mapfile_internal.head";
62
my $mapfile_internal_tail	= "$mapfile_internal.tail";
63 64 65
my $vnodesfile			= "$mapdir/vnodes.${OURDOMAIN}.db";
my $vnodesback 			= "$mapdir/vnodes.${OURDOMAIN}.db.backup";
my $reversedir			= "$mapdir/reverse";
66
my $restart_named		= 1;
67
my $sortem			= 0; # set to 1 to generated IP-sorted file
68
my $dbg	= 0;
69
my $domx = 0;
70 71
my @row;

72
# If we are disabled, just quietly exit
73 74 75 76
if ($DISABLED) {
    exit 0;
}

77
# We do not want to run this script unless its the real version.
78
if ($EUID != 0) {
79 80
    die("*** $0:\n".
	"    Must be root! Maybe its a development version?\n");
81 82
}
# XXX Hacky!
83
if (0 && $TB ne "/usr/testbed") {
84 85
    die("*** $0:\n".
	"    Wrong version. Maybe its a development version?\n");
86 87
}

88 89 90 91
# un-taint path
$ENV{'PATH'} = '/bin:/usr/bin:/usr/sbin:/usr/local/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

92 93
# Turn off line buffering on output
$| = 1;
94

95
# Testbed Support libraries
96 97 98
use lib "@prefix@/lib";
use libtestbed;
use libdb;
99

100 101 102 103 104 105 106
#
# Check for norestart option.
#
if (@ARGV && $ARGV[0] eq "-norestart") {
    $restart_named = 0;
}

107
#
108
# We need to serialize this script to avoid a trashed map file.
109
#
110 111 112 113
if ((my $locked = TBScriptLock("named", 1)) != TBSCRIPTLOCK_OKAY()) {
    exit(0)
        if ($locked == TBSCRIPTLOCK_IGNORE);
    fatal("Could not get the lock after a long time!\n");
114

115 116 117
}

#
118
# Grab the list of all nodes with proper control network interfaces.
119
#
120
my $db_result =
121
    DBQueryFatal("select n.node_id, n.role, null, i.IP, i.role, ".
122
                 "    r.inner_elab_role, i.IPaliases ".
123 124
		 "  from nodes as n join interfaces as i ".
		 "left join reserved as r on r.node_id=n.node_id ".
125 126 127
		 "where (n.node_id=i.node_id and ".
                 "       (n.role='testnode' or n.role='virtnode')) ".
		 "    and i.IP is not null ".
128 129
		 "    and (i.role='" . TBDB_IFACEROLE_CONTROL() . "' or " .
		 "         i.role='" . TBDB_IFACEROLE_MANAGEMENT() . "')");
130

131
my %routable;
132 133
my %unroutable;
my %reverse;
134

135
process_nodes($db_result);
136

137 138 139 140 141 142 143
#
# Now get the static virtnodes and older dynamic virtnodes that used
# jailip. The i2 join is to weed out the dynamic virtnodes we got in
# the above query (nodes with a dynamic control network interface).
#
$db_result =
    DBQueryFatal("select n.node_id, n.role, n.jailip, i.IP, i.role, ".
144
                 "    r.inner_elab_role, null ".
145 146 147 148 149 150 151 152 153 154
                 "  from nodes as n ".
                 "left join interfaces as i on i.node_id=n.phys_nodeid ".
                 "left join interfaces as i2 on i2.node_id=n.node_id ".
                 "left join reserved as r on r.node_id=n.node_id ".
                 "where n.role='virtnode' and i2.card is null and ".
                 "      (i.IP is not null or n.jailip is not null) and ".
                 "      i.role='" . TBDB_IFACEROLE_CONTROL() . "'");

process_nodes($db_result);

155 156 157 158
#
# For IXPs we also need their gateway addresses as well
#
$db_result =
159
    DBQueryFatal("select n.node_id, n.role, n.jailip, i.IP, i.role, null,null ".
160
		 "  from nodes as n ".
161 162 163 164 165 166 167
		 "left join interfaces as i ".
		 "on n.phys_nodeid=i.node_id and n.node_id!=i.node_id ". 
		 "where n.role='testnode' ".
		 "    and (i.IP is not null or n.jailip is not null) ".
		 "    and (i.card is null or ".
		 "         i.role='" . TBDB_IFACEROLE_GW() . "') ");
process_nodes($db_result);
168

169 170 171 172 173 174 175 176 177 178 179 180 181
# Get the v2pmap table since that holds the additional name mappings.
my %p2vmap = ();
$db_result =
    DBQueryFatal("select v.vname,v.node_id from reserved as r ".
                 "left join v2pmap as v on v.node_id=r.node_id and ".
                 "     v.exptidx=r.exptidx and v.vname!=r.vname ".
                 "where v.vname is not null");
while (my ($vname,$node_id) = $db_result->fetchrow_array()) {
    $p2vmap{$node_id} = []
        if (!exists($p2vmap{$node_id}));
    push(@{ $p2vmap{$node_id} }, $vname);
}

182
#
183
# Get the list of currently-reserved nodes so that we can make CNAMEs for them
184
#
185
$db_result =
186
    DBQueryFatal("select node_id,pid,eid,vname,inner_elab_role from reserved");
187

188
my %cnames;
189
while (my ($node_id,$pid,$eid,$vname,$erole) = $db_result->fetchrow_array()) {
190

191 192 193 194 195 196
    #
    # Handle some rare cases where a node can get reserved without a vname -
    # such as calling nalloc directly
    #
    if (!defined($vname)) {
	$vname = $node_id;
197
    }
198
    push @{$cnames{$node_id}}, "$vname.$eid.$pid";
199

200 201 202 203 204 205 206 207 208
    # Temp hack for Leigh
    if ($pid eq "testbed" and $eid eq "xen-leelab") {
        push @{$cnames{$node_id}}, "${vname}.xenlab.$pid";

        if (defined($erole) && $erole =~ /boss/) {
            push @{$cnames{$node_id}}, "www.xenlab.$pid";
        }
    }

209 210 211
    #
    # Special case for inner elab boss; add CNAME for www.
    #
212
    if (defined($erole) && $erole =~ /boss/) {
213 214
        push @{$cnames{$node_id}}, "www.$eid.$pid";
    }
215 216 217 218 219 220 221
    #
    # Special case for inner elab ops; add CNAME for event-server.
    #
    if (defined($erole) &&
        ($erole eq "ops" || $erole eq "ops+fs")) {
        push @{$cnames{$node_id}}, "event-server.$eid.$pid";
    }
222

223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
    #
    # And add additional names in v2pmap, which can happen for delay and
    # bridge nodes.
    #
    if (exists($p2vmap{$node_id})) {
        my @extras = @{ $p2vmap{$node_id} };

	foreach my $extra (@extras) {
            next
		if ($extra eq $vname);

	    push @{$cnames{$node_id}}, "$extra.$eid.$pid";
	}
    }
}
238 239

#
240
# Make the zone file for routable IP addresses
241
#
242 243
make_forward_zonefile($mapfiletail,\%routable,\%cnames);
assemble_zonefile($mapfile);
244

245
#
246 247
# Make the zone file that includes both routable and unroutable IP addresses,
# if the site has a .head file for it
248 249
#
if (-e $mapfile_internal_head) {
250 251 252
    make_forward_zonefile($mapfile_internal_tail,
	{%routable, %unroutable},\%cnames);
    assemble_zonefile($mapfile_internal);
253
}
254

255

256 257 258 259 260
#
# Look for reverse zone files that we may need to make
#
opendir(DIR,$reversedir) or fatal("Unable to open directory $reversedir\n");
while (my $dirent = readdir(DIR)) {
261 262
    if (! (($dirent =~ /((\d+\.\d+\.\d+).*\.db)\.head/) ||
           ($dirent =~ /((\d+\.\d+).*\.db)\.head/))) {
263 264 265 266 267 268 269
	next;
    }
    my $subnet = $2;
    my $basename = $1;

    my $filename = "$reversedir/$basename.tail";
    open MAP, ">$filename" || fatal("Couldn't open $filename: $!\n");
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
    if (exists($reverse{$subnet})) {
        if ($subnet =~ /^(\d+)\.(\d+)$/) {
            my $classb = $reverse{$subnet};
            foreach my $classc (keys(%{$reverse{$subnet}})) {
                print MAP "\$ORIGIN ${classc}.${2}.${1}.in-addr.arpa.\n";
                foreach my $aref (@{$reverse{$subnet}->{$classc}}) {
	            my ($host, $name) = @$aref;
	            printf MAP "$host\tIN\tPTR\t$name.$OURDOMAIN.\n";
                }
                print MAP "\n";
            }
        }
        else {
	    foreach my $aref (sort {$$a[0] <=> $$b[0]} @{$reverse{$subnet}}) {
	        my ($host, $name) = @$aref;
	        printf MAP "$host\tIN\tPTR\t$name.$OURDOMAIN.\n";
	    }
        }
288 289 290
    }
    close MAP;

291
    assemble_zonefile("$reversedir/$basename");
292 293 294 295
    
}
closedir DIR;

296
#
297
# Get the nameserver to reload the zone files.
298
# This is better than HUPing the nameserver directly. Note that we look
299 300
# for a local port of named first.
#
301 302 303 304
if ($restart_named) {
    if (-x "/usr/local/sbin/rndc") {
	system("/usr/local/sbin/rndc reload > /dev/null") == 0 or
	    fatal("/usr/local/sbin/rndc reload failed!\n");
305 306 307 308 309 310 311 312 313 314
    }
    # XXX named.reload went away circa FBSD 9
    elsif (-x "/usr/sbin/named.reload") {
	system("/usr/sbin/named.reload > /dev/null") == 0 or
	    fatal("/usr/sbin/named.reload failed!\n");
    }
    # try standard rndc
    else {
	system("/usr/sbin/rndc reload > /dev/null") == 0 or
	    fatal("/usr/sbin/rndc failed!\n");
315
    }
316
}
317 318
TBScriptUnlock();
exit(0);
319

320 321 322 323 324 325 326 327 328 329 330
#
# Sort out the routable and unroutable addresses from a DB query,
# and make a map for reversing them
#
sub process_nodes($) {
    while (my @row = $db_result->fetchrow_array()) {
	my $node_id = $row[0];
	my $nrole   = $row[1];
	my $jailIP  = $row[2];
	my $IP      = $row[3];
	my $irole   = $row[4];
331
	my $inner_elab_role = $row[5];
332
	my $IPaliases = $row[6];
333 334 335 336 337 338 339 340 341 342 343 344 345

	#
	# For most nodes, we get the IP address from the interfaces table;
	# but, for virtual nodes, we get it from the jailip column
	#
	if (defined($jailIP)) {
	    $IP = $jailIP;
	}
	if (!$IP) {
	    warn "named_setup: No IP for node $node_id!\n";
	    next;
	}

346
        #
347 348 349 350
	# Special treatment for gateway interfaces - we give act as if they
	# are a separate node
	#
	if ($irole && $irole eq TBDB_IFACEROLE_GW()) {
351 352 353 354 355
	    $node_id = "${node_id}-gw";
	}
	# Ditto for management interface.
	if ($irole && $irole eq TBDB_IFACEROLE_MANAGEMENT()) {
	    $node_id = "${node_id}-mng";
356 357 358 359 360 361 362 363
	}

	#
	# Make a little structure so that we can make decisions later about
	# this node (ie. handling virt nodes differently)
	#
	my $node_rec = {
	    IP   => $IP,
364 365
	    role => $nrole,
	    inner_elab_role => $inner_elab_role
366 367 368 369 370 371 372 373 374 375 376
	};

	#
	# Sort it into the right pile based on whether or not it's routable
	#
	if (isroutable($IP)) {
	    $routable{$node_id} = $node_rec;
	} else {
	    $unroutable{$node_id} = $node_rec;
	}

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
	#
	# Special treatment for virtnodes with private IP addresses;
	# If there is an IPaliases defined, add a entry for that too,
	# but only to the other map. In other words, if the IP is
	# unroutable and the ipalias is routable, then we can have
	# two entries, one of which is routable.
	#
	if (defined($IPaliases) && $IPaliases ne "") {
	    my @ipaliases = split(',', $IPaliases);
	    my $ipalias   = $ipaliases[0];

	    if (!isroutable($IP) && isroutable($ipalias)) {
		$routable{$node_id} = {
		    IP   => $ipalias,
		    role => $nrole,
		    inner_elab_role => $inner_elab_role
		};
	    }
	}

397 398 399
	#
	# Put it into a map so we can generate the reverse zone file later
	#
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
	# We use a Class C for the jail network, which complicates the
	# reverse zone generation.
	#
	if (IsJailIP($IP)) {
	    $IP =~ /(\d+\.\d+)\.(\d+)\.(\d+)/;
	    my $classb = $1;
	    my $subnet = $2;
	    my $host   = $3;

	    if (!exists($reverse{"$classb"})) {
		$reverse{"$classb"} = {};
	    }
	    if (!exists($reverse{"$classb"}->{$subnet})) {
		$reverse{"$classb"}->{$subnet} = [];
	    }
	    push(@{$reverse{"$classb"}->{$subnet}}, [$host, $node_id]);
	}
	elsif ($IP =~ /(\d+\.\d+\.\d+)\.(\d+)/) {
418 419 420
	    my $subnet = $1;
	    my $host = $2;
	    push @{$reverse{$subnet}}, [$host, $node_id];
421 422
	}
	else {
423 424 425 426 427
	    warn "Poorly formed IP address $IP\n";
	}
    }
}

428 429 430
#
# Die and tell someone about it
#
431
sub fatal {
432
    my $msg = $_[0];
433

434
    TBScriptUnlock();
435
    SENDMAIL($TBOPS, "Named Setup Failed", $msg);
436 437
    die($msg);
}
438 439

#
440
# Put together a zone file from its consituent head and tail pieces
441
#
442 443
sub assemble_zonefile($) {
    my ($mapfile) = @_;
444 445

    my $mapfileback = "$mapfile.backup";
446 447
    my $mapfilehead = "$mapfile.head";
    my $mapfiletail = "$mapfile.tail";
448
    my $mapfilefrag = "$mapfile.local";
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466

    #
    # Concat the head and tail files to create the new map.
    #
    if (-e $mapfile) {
	system("mv -f $mapfile $mapfileback") == 0 or
	fatal("Could not back up $mapfile to $mapfileback\n");
    }

    #
    # Generate a warning so that no one tries to edit the file by hand
    #
    open(MAP, ">$mapfile") || fatal("Couldn't open $mapfile\n");
    print MAP
    ";\n".
    "; ******************************************************************\n".
    "; DO NOT EDIT THIS FILE. IT IS A CREATION, A FIGMENT, A CONTRIVANCE!\n".
    ";\n".
467
    "; Edit the \"head\" file, then run ${TB}/bin/named_setup.\n".
468 469 470 471 472 473 474 475 476 477
    "; ******************************************************************\n".
    ";\n";

    #
    # Now copy in the head part of the map, looking for the serial
    # number so it can be bumped up.
    #
    open(MAPHEAD, "<$mapfilehead") || fatal("Couldn't open $mapfilehead\n");
    while (<MAPHEAD>) {
	if ( /;\s*Serial\s+/i ) {
478 479 480
	    my $serial = `date +%s`;
	    chomp($serial);
	    $serial = $serial | 0x70000000;
481 482 483 484 485 486 487 488 489 490

	    print MAP "\t\t\t$serial\t; Serial Number -- DO NOT EDIT\n";
	}
	else {
	    print MAP "$_";
	}
    }
    close(MAPHEAD);
    close(MAP);

491 492 493 494 495 496 497 498 499 500 501 502
    # Give local admin a place to add static stuff to the head.
    if (-e $mapfilefrag) {
	system("echo '' >> $mapfile");
	system("echo ';' >> $mapfile");
	system("echo '; This is a local fragment; $mapfilefrag' >> $mapfile");
	system("echo ';' >> $mapfile");
	system("cat $mapfilefrag >> $mapfile") == 0 or
	    fatal("Failed to concat $mapfilefrag to $mapfile\n");
	system("echo '; End of local fragment; $mapfilefrag' >> $mapfile");
	system("echo '' >> $mapfile");
    }

503 504 505 506
    #
    # Now the tail of the map.
    # 
    system("cat $mapfiletail >> $mapfile") == 0 or
507
	fatal("Failed to concat $mapfiletail to $mapfile\n");
508
}
509

510 511 512 513 514 515 516 517 518 519
my $laddrs;
sub byip {
    my @aa = split '\.', $$laddrs{$a}->{IP};
    my @bb = split '\.', $$laddrs{$b}->{IP};
    return $aa[0] <=> $bb[0] ||
	$aa[1] <=> $bb[1] ||
	    $aa[2] <=> $bb[2] ||
		$aa[3] <=> $bb[3];
}

520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
#
# Make a forward zone file, from the given map of addresses and CNAMEs
#
sub make_forward_zonefile($$$) {
    my ($filename, $addresses, $cnames) = @_;
    open(MAP, ">$filename") || fatal("Couldn't open $filename\n");
    print MAP "\n";
    print MAP ";\n";
    print MAP "; DO NOT EDIT below this point. Auto generated map entries!\n";
    print MAP ";\n";
    print MAP "\n";

    #
    # Start out with the A records for the nodes
    #
    print MAP "\n";
    print MAP ";\n";
    print MAP "; Nodes\n";
    print MAP ";\n";
    print MAP "\n";
540 541 542 543 544 545 546
    my @list = keys(%$addresses);
    if ($sortem) {
	$laddrs = $addresses;
	@list = sort byip @list;
    }
    for my $node_id (@list) {
	my $node_rec = $$addresses{$node_id};
547 548 549 550 551 552 553 554 555 556

	#
	# Special treatment for virtual nodes - we only bother to print them
	# out if some has reserved them (ie. if they have a CNAME)
	#
	if (($node_rec->{role} eq "virtnode") && (!$cnames->{$node_id})) {
	    next;
	}

	print MAP "$node_id\tIN\tA\t$node_rec->{IP}\n";
557 558 559 560 561 562 563 564 565
	if ($domx) {
	    if (defined($node_rec->{inner_elab_role}) &&
		($node_rec->{inner_elab_role} eq "ops" ||
		 $node_rec->{inner_elab_role} eq "ops+fs")) {
		print MAP "\tIN\tMX 10\t$node_id\n";
	    }
	    else {
		print MAP "\tIN\tMX 10\t$USERS.\n";
	    }
566
	}
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
    }

    #
    # Switch the TTL to 1 second for CNAMEs so that people will changes quickly
    # as experiments swap in and out
    #
    print MAP "\n";
    print MAP "\$TTL\t1\n\n";

    print MAP "\n";
    print MAP ";\n";
    print MAP "; CNAMEs for reserved nodes\n";
    print MAP ";\n";
    print MAP "\n";

    while (my ($pname, $vnames) = each %$cnames) {
	#
	# Only print out CNAMEs for nodes that are actually going into this map
	#
	next unless ($addresses->{$pname});

	#
	# Write out every CNAME for this pnode
	#
	foreach my $vname (@$vnames) {
	    my $formatted_vname = sprintf "%-50s", $vname;
	    print MAP "$formatted_vname\tIN\tCNAME\t$pname\n";
	}
    }

    print MAP "\n";
    close(MAP);
}

601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627
#
# Is an IP routable?
#
sub isroutable($)
{
    my ($IP)  = @_;
    my ($a,$b,$c,$d) = ($IP =~ /^(\d*)\.(\d*)\.(\d*)\.(\d*)/);

    #
    # These are unroutable:
    # 10.0.0.0        -   10.255.255.255  (10/8 prefix)
    # 172.16.0.0      -   172.31.255.255  (172.16/12 prefix)
    # 192.168.0.0     -   192.168.255.255 (192.168/16 prefix)
    #

    # Easy tests.
    return 0
	if (($a eq "10") ||
	    ($a eq "192" && $b eq "168"));

    # Lastly
    return 0
	if (inet_ntoa((inet_aton($IP) & inet_aton("255.240.0.0"))) eq
	    "172.16.0.0");

    return 1;
}
628 629 630 631 632 633 634 635 636 637 638 639 640 641 642

#
# IsJailIP()
#
sub IsJailIP($)
{
    my ($IP)  = @_;

    return 1
	if (inet_ntoa((inet_aton($IP) & inet_aton($VIRTNODE_NETMASK))) eq
	    $VIRTNODE_NETWORK);

    return 0;
}