package xCAT_plugin::dns; use strict; use Getopt::Long; use Net::DNS; use xCAT::Table; use Sys::Hostname; use MIME::Base64; use Socket; use Fcntl qw/:flock/; #This is a rewrite of DNS management using nsupdate rather than direct zone mangling my $callback; sub handled_commands { return {"makedns" => "dns"}; } sub get_reverse_zone_for_entity { my $ctx = shift; my $node = shift; my $net; if ($ctx->{hoststab} and $ctx->{hoststab}->{$node} and $ctx->{hoststab}->{$node}->[0]->{ip}) { $node = $ctx->{hoststab}->{$node}->[0]->{ip}; } my $tvar; if ($tvar = inet_aton($node)) { #This is an assignment, we are testing and storing the value in one shot $tvar = unpack("N",$tvar); foreach my $net (keys %{$ctx->{nets}}) { if ($ctx->{nets}->{$net}->{netn} == ($tvar & $ctx->{nets}->{$net}->{mask})) { my $maskstr = unpack("B32",pack("N",$ctx->{nets}->{$net}->{mask})); my $maskcount = ($maskstr =~ tr/1//); $maskcount+=((8-($maskcount%8))%8); #round to the next octet my $newmask = 2**$maskcount -1 << (32 - $maskcount); my $rev = inet_ntoa(pack("N",($tvar & $newmask))); my @zone; my @orig=split /\./,$rev; while ($maskcount) { $maskcount-=8; unshift(@zone,(shift @orig)); } $rev = join('.',@zone); $rev .= '.IN-ADDR.ARPA'; return $rev; } } } return undef; } sub process_request { my $request = shift; $callback = shift; umask 0007; my $ctx = {}; my @nodes=(); my $hadargs=0; my $allnodes; my $zapfiles; if ($request->{arg}) { $hadargs=1; @ARGV=@{$request->{arg}}; if (!GetOptions( 'a|all' => \$allnodes, 'n|new' => \$zapfiles, )) { sendmsg([1,"TODO: makedns Usage message"]); return; } } my $sitetab = xCAT::Table->new('site'); my $stab = $sitetab->getAttribs({key=>'domain'},['value']); unless ($stab and $stab->{value}) { sendmsg([1,"domain not defined in site table"]); return; } $ctx->{domain} = $stab->{value}; if ($request->{node}) { #we have a noderange to process @nodes = @{$request->{node}}; } elsif ($allnodes) { #read all nodelist specified nodes } else { #legacy behavior, read from /etc/hosts my $hostsfile; open($hostsfile,"<","/etc/hosts"); flock($hostsfile,LOCK_SH); my @contents = <$hostsfile>; flock($hostsfile,LOCK_UN); close($hostsfile); my $domain = $ctx->{domain}; unless ($domain =~ /^\./) { $domain = '.'.$domain; } my $addr; my $name; my $canonical; my $aliasstr; my @aliases; my $names; foreach (@contents) { chomp; #no newline s/#.*//; #strip comments; s/^[ \t\n]*//; #remove leading whitespace next unless ($_); #skip empty lines ($addr,$names) = split /[ \t]+/,$_,2; if ($addr !~ /^\d+\.\d+\.\d+\.\d+$/) { sendmsg(":Ignoring line $_ in /etc/hosts, only IPv4 format entries are supported currently"); next; } unless ($names =~ /^[a-z0-9\. \t\n-]+$/i) { sendmsg(":Ignoring line $_ in /etc/hosts, names $names contain invalid characters (valid characters include a through z, numbers and the '-', but not '_'"); next; } ($canonical,$aliasstr) = split /[ \t]+/,$names,2; if ($aliasstr) { @aliases= split /[ \t]+/,$aliasstr; } else { @aliases = (); } my %names = (); my $node = $canonical; unless ($canonical =~ /$domain/) { $canonical.=$domain; } unless ($canonical =~ /\.\z/) { $canonical .= '.' } #for only the sake of comparison, ensure consistant dot suffix foreach my $alias (@aliases) { unless ($alias =~ /$domain/) { $alias .= $domain; } unless ($alias =~ /\.\z/) { $alias .= '.'; } if ($alias eq $canonical) { next; } $ctx->{aliases}->{$node}->{$alias}=1; #remember alias for CNAM records later } push @nodes,$node; $ctx->{nodeips}->{$node}->{$addr}=1; } } my $hoststab = xCAT::Table->new('hosts',-create=>0); if ($hoststab) { $ctx->{hoststab} = $hoststab->getNodesAttribs(\@nodes,['ip']); } $ctx->{nodes} = \@nodes; my $networkstab = xCAT::Table->new('networks',-create=>0); unless ($networkstab) { sendmsg([1,'Unable to enumerate networks, try to run makenetworks']); } my @networks = $networkstab->getAllAttribs('net','mask'); foreach (@networks) { my $maskn = unpack("N",inet_aton($_->{mask})); $ctx->{nets}->{$_->{net}}->{mask} = $maskn; $ctx->{nets}->{$_->{net}}->{netn} = unpack("N",inet_aton($_->{net})); } my $passtab = xCAT::Table->new('passwd'); my $pent = $passtab->getAttribs({key=>'omapi',username=>'xcat_key'},['password']); if ($pent and $pent->{password}) { $ctx->{privkey} = $pent->{password}; } #do not warn/error here yet, if we can't generate or extract, we'll know later $stab = $sitetab->getAttribs({key=>'forwarders'},['value']); if ($stab and $stab->{value}) { my @forwarders = split /[ ,]/,$stab->{value}; $ctx->{forwarders}=\@forwarders; } $ctx->{zonestotouch}->{$ctx->{domain}}=1; foreach (@nodes) { my $revzone = get_reverse_zone_for_entity($ctx,$_);; unless ($revzone) { next; } $ctx->{revzones}->{$_} = $revzone; $ctx->{zonestotouch}->{$ctx->{revzones}->{$_}}=1; } if (1) { #TODO: function to detect and return 1 if the master server is DNS SOA for all the zones we care about #here, we are examining local files to assure that our key is in named.conf, the zones we care about are there, and that if #active directory is in use, allow the domain controllers to update specific zones $stab =$sitetab->getAttribs({key=>'directoryprovider'},['value']); if ($stab and $stab->{value} and $stab->{value} eq 'activedirectory') { $stab =$sitetab->getAttribs({key=>'directoryservers'},['value']); if ($stab and $stab->{value} and $stab->{value}) { my @dservers = split /[ ,]/,$stab->{value}; $ctx->{adservers} = \@dservers; $ctx->{adzones} = { "_msdcs.". $ctx->{domain} => 1, "_sites.". $ctx->{domain} => 1, "_tcp.". $ctx->{domain} => 1, "_udp.". $ctx->{domain} => 1, }; } } $stab =$sitetab->getAttribs({key=>'dnsupdaters'},['value']); #allow unsecure updates from these if ($stab and $stab->{value} and $stab->{value}) { my @nservers = split /[ ,]/,$stab->{value}; $ctx->{dnsupdaters} = \@nservers; } if ($zapfiles) { #here, we unlink all the existing files to start fresh system("/sbin/service named stop"); #named may otherwise hold on to stale journal filehandles unlink "/etc/named.conf"; foreach () { unlink $_; } foreach () { unlink $_; } } #We manipulate local namedconf $ctx->{dbdir} = get_dbdir(); chmod 0775, $ctx->{dbdir}; # assure dynamic dns can actually execute against the directory update_namedconf($ctx); update_zones($ctx); if ($ctx->{restartneeded}) { sendmsg("Restarting named"); system("/sbin/service named start"); system("/sbin/service named reload"); sendmsg("Restarting named complete"); } } else { unless ($ctx->{privkey}) { sendmsg([1,"Unable to update DNS due to lack of credentials in passwd to communicate with remote server"]); } } #now we stick to Net::DNS style updates, with TSIG if possible. TODO: kerberized (i.e. Windows) DNS server support, maybe needing to use nsupdate -g.... $ctx->{resolver} = Net::DNS::Resolver->new(); add_records($ctx); } sub get_dbdir { if (-d "/var/named") { return "/var/named/"; } elsif (-d "/var/lib/named") { return "/var/lib/named/"; } else { use File::Path; mkpath "/var/named/"; chown(scalar(getpwnam('named')),scalar(getgrnam('named')),"/var/named"); return "/var/named/"; } } sub update_zones { my $ctx = shift; my $currzone; my $dbdir = $ctx->{dbdir}; my $domain = $ctx->{domain}; my $name = hostname; my $node = $name; unless ($domain =~ /^\./) { $domain = '.'.$domain; } unless ($name =~ /\./) { $name .= $domain; } unless ($name =~ /\.\z/) { $name .= '.'; } my $ip=$node; if ($ctx->{hoststab} and $ctx->{hoststab}->{$node} and $ctx->{hoststab}->{$node}->[0]->{ip}) { $ip = $ctx->{hoststab}->{$node}->[0]->{ip}; } else { unless ($ip = inet_aton($ip)) { print "Unable to find an IP for $node in hosts table or via system lookup (i.e. /etc/hosts"; sendmsg([1,"Unable to find an IP for $node in hosts table or via system lookup (i.e. /etc/hosts"]); next; } $ip = inet_ntoa($ip); } my @neededzones = keys %{$ctx->{zonestotouch}}; push @neededzones,keys %{$ctx->{adzones}}; my ($sec, $min, $hour, $mday, $mon, $year, $rest) = localtime(time); my $serial = ($mday * 100) + (($mon + 1) * 10000) + (($year + 1900) * 1000000); foreach $currzone (@neededzones) { my $zonefilename = $currzone; if ($currzone =~ /IN-ADDR\.ARPA/) { $currzone =~ s/\.IN-ADDR\.ARPA.*//; my @octets = split/\./,$currzone; $currzone = join('.',reverse(@octets)); $zonefilename = $currzone; } else { $zonefilename =~ s/\..*//; #compatible with bind.pm } unless (-f $dbdir."/db.$zonefilename") { my $zonehdl; open($zonehdl,">>",$dbdir."/db.$zonefilename"); flock($zonehdl,LOCK_EX); seek($zonehdl,0,0); truncate($zonehdl,0); print $zonehdl '$TTL 86400'."\n"; print $zonehdl '@ IN SOA '.$name." root.$name ( $serial 10800 3600 604800 86400 )\n"; print $zonehdl " IN NS $name\n"; if ($name =~ /$currzone/) { #Must guarantee an A record for the DNS server print $zonehdl "$name IN A $ip\n"; } flock($zonehdl,LOCK_UN); close($zonehdl); chown(scalar(getpwnam('named')),scalar(getgrnam('named')),$dbdir."/db.$zonefilename"); $ctx->{restartneeded}=1; } } } sub update_namedconf { my $ctx = shift; my $namedlocation = '/etc/named.conf'; my $nameconf; my @newnamed; my $gotoptions=0; my $gotkey=0; my %didzones; if (-r $namedlocation) { my @currnamed=(); open($nameconf,"<",$namedlocation); flock($nameconf,LOCK_SH); @currnamed=<$nameconf>; flock($nameconf,LOCK_UN); close($nameconf); my $i = 0; for ($i=0;$i{forwarders} and $line =~ /forwarders {/) { push @newnamed,"\tforwarders \{\n"; $skip=1; foreach (@{$ctx->{forwarders}}) { push @newnamed,"\t\t".$_.";\n"; } push @newnamed,"\t};\n"; } elsif ($skip) { if ($line =~ /};/) { $skip = 0; } } else { push @newnamed,$line; } $i++; $line = $currnamed[$i]; } while ($line !~ /^\};/); push @newnamed,$line; } elsif ($line =~ /^zone "([^"]*)" in \{/) { my $currzone = $1; if ($ctx->{zonestotouch}->{$currzone} or $ctx->{adzones}->{$currzone}) { $didzones{$currzone}=1; my @candidate = ($line); my $needreplace=1; do { $i++; $line = $currnamed[$i]; push @candidate,$line; if ($line =~ /key xcat_key/) { $needreplace=0; } } while ($line !~ /^\};/); #skip the old file zone unless ($needreplace) { push @newnamed,@candidate; next; } $ctx->{restartneeded}=1; push @newnamed,"zone \"$currzone\" in {\n","\ttype master;\n","\tallow-update {\n","\t\tkey xcat_key;\n"; my @list; if (not $ctx->{adzones}->{$currzone}) { if ($ctx->{dnsupdaters}) { @list = @{$ctx->{dnsupdaters}}; } } else { if ($ctx->{adservers}) { @list = @{$ctx->{adservers}}; } } foreach (@list) { push @newnamed,"\t\t$_;\n"; } if ($currzone =~ /IN-ADDR\.ARPA/) { my $net = $currzone; $net =~ s/.IN-ADDR\.ARPA.*//; my @octets = split/\./,$net; $net = join('.',reverse(@octets)); push @newnamed,"\t};\n","\tfile \"db.$net\";\n","};\n"; } else { my $zfilename = $currzone; $zfilename =~ s/\..*//; push @newnamed,"\t};\n","\tfile \"db.$zfilename\";\n","};\n"; } } else { push @newnamed,$line; do { $i++; $line = $currnamed[$i]; push @newnamed,$line; } while ($line !~ /^\};/); } } elsif ($line =~ /^key xcat_key/) { $gotkey=1; if ($ctx->{privkey}) { #for now, assume the field is correct #push @newnamed,"key xcat_key {\n","\talgorithm hmac-md5;\n","\tsecret \"".$ctx->{privkey}."\";\n","};\n\n"; push @newnamed,$line; do { $i++; $line = $currnamed[$i]; push @newnamed,$line; } while ($line !~ /^\};/); } else { push @newnamed,$line; while ($line !~ /^\};/) { #skip the old file zone if ($line =~ /secret \"([^"]*)\"/) { my $passtab = xCAT::Table->new("passwd",-create=>1); $passtab->setAttribs({key=>"omapi",username=>"xcat_key"},{password=>$1}); } $i++; $line = $currnamed[$i]; push @newnamed,$line; } } } else { push @newnamed,$line; } } } unless ($gotoptions) { push @newnamed,"options {\n","\tdirectory \"".$ctx->{dbdir}."\";\n"; if ($ctx->{forwarders}) { push @newnamed,"\tforwarders {\n"; foreach (@{$ctx->{forwarders}}) { push @newnamed,"\t\t$_;\n"; } push @newnamed,"\t};\n"; } push @newnamed,"};\n\n"; } unless ($gotkey) { unless ($ctx->{privkey}) { #need to generate one $ctx->{privkey} = encode_base64(genpassword(32)); chomp($ctx->{privkey}); } push @newnamed,"key xcat_key {\n","\talgorithm hmac-md5;\n","\tsecret \"".$ctx->{privkey}."\";\n","};\n\n"; $ctx->{restartneeded}=1; } my $zone; foreach $zone (keys %{$ctx->{zonestotouch}}) { if ($didzones{$zone}) { next; } $ctx->{restartneeded}=1; #have to add a zone, a restart will be needed push @newnamed,"zone \"$zone\" in {\n","\ttype master;\n","\tallow-update {\n","\t\tkey xcat_key;\n"; foreach (@{$ctx->{dnsupdaters}}) { push @newnamed,"\t\t$_;\n"; } if ($zone =~ /IN-ADDR\.ARPA/) { my $net = $zone; $net =~ s/.IN-ADDR\.ARPA.*//; my @octets = split/\./,$net; $net = join('.',reverse(@octets)); push @newnamed,"\t};\n","\tfile \"db.$net\";\n","};\n"; } else { my $zfilename = $zone; $zfilename =~ s/\..*//; push @newnamed,"\t};\n","\tfile \"db.$zfilename\";\n","};\n"; } } foreach $zone (keys %{$ctx->{adzones}}) { if ($didzones{$zone}) { next; } $ctx->{restartneeded}=1; #have to add a zone, a restart will be needed push @newnamed,"zone \"$zone\" in {\n","\ttype master;\n","\tallow-update {\n","\t\tkey xcat_key;\n"; foreach (@{$ctx->{adservers}}) { push @newnamed,"\t\t$_;\n"; } my $zfilename = $zone; $zfilename =~ s/\..*//; push @newnamed,"\t};\n","\tfile \"db.$zfilename\";\n","};\n\n"; } my $newnameconf; open($newnameconf,">>",$namedlocation); flock($newnameconf,LOCK_EX); seek($newnameconf,0,0); truncate($newnameconf,0); for my $l (@newnamed) { print $newnameconf $l; } flock($newnameconf,LOCK_UN); close($newnameconf); chown (scalar(getpwnam('root')),scalar(getgrnam('named')),$namedlocation); } sub add_records { my $ctx = shift; unless ($ctx->{privkey}) { my $passtab = xCAT::Table->new('passwd'); my $pent = $passtab->getAttribs({key=>'omapi',username=>'xcat_key'},['password']); if ($pent and $pent->{password}) { $ctx->{privkey} = $pent->{password}; } else { sendmsg([1,"Unable to find omapi key in passwd table"]); } } my $node; my $ip; my $domain = $ctx->{domain}; # store off for lazy typing and possible local mangling unless ($domain =~ /^\./) { $domain = '.'.$domain; } #example.com becomes .example.com for consistency $ctx->{nsmap} = {}; #will store a map to known NS records to avoid needless redundant queries to sort nodes into domains $ctx->{updatesbyzone}={}; #sort all updates into their respective zones for bulk update for fewer DNS transactions foreach $node (@{$ctx->{nodes}}) { $ip = $node; my $name = $node; unless ($name =~ /$domain/) { $name .= $domain } # $name needs to represent fqdn, but must preserve $node as a nodename for cfg lookup #if (domaintab->{$node}->[0]->{domain) { $domain = domaintab->{$node}->[0]->{domain) } #above is TODO draft of how multi-domain support could come into play if ($ctx->{hoststab} and $ctx->{hoststab}->{$node} and $ctx->{hoststab}->{$node}->[0]->{ip}) { $ip = $ctx->{hoststab}->{$node}->[0]->{ip}; } else { unless ($ip = inet_aton($ip)) { sendmsg([1,"Unable to find an IP for $node in hosts table or via system lookup (i.e. /etc/hosts"]); next; } $ip = inet_ntoa($ip); } $ctx->{currip}=$ip; #time to update, A and PTR records, IPv6 still TODO $ip = join('.',reverse(split(/\./,$ip))); $ip .= '.IN-ADDR.ARPA.'; #ok, now it is time to identify which zones should actually hold the forward (A) and reverse (PTR) records and a nameserver to handle the request my $revzone = $ip; $ctx->{currnode}=$node; $ctx->{currname}=$name; $ctx->{currrevname}=$ip; find_nameserver_for_dns($ctx,$revzone); find_nameserver_for_dns($ctx,$domain); } my $zone; foreach $zone (keys %{$ctx->{updatesbyzone}}) { my $resolver = Net::DNS::Resolver->new(nameservers=>[$ctx->{nsmap}->{$zone}]); my $entry; my $numreqs = 300; # limit to 300 updates in a payload, something broke at 644 on a certain sample, choosing 300 for now my $update = Net::DNS::Update->new($zone); foreach $entry (@{$ctx->{updatesbyzone}->{$zone}}) { $update->push(update=>rr_add($entry)); $numreqs -= 1; if ($numreqs == 0) { $update->sign_tsig("xcat_key",$ctx->{privkey}); $numreqs=300; my $reply = $resolver->send($update); if ($reply->header->rcode ne 'NOERROR') { sendmsg([1,"Failure encountered updating $zone, error was ".$reply->header->rcode]); } $update = Net::DNS::Update->new($zone); #new empty request } } if ($numreqs != 300) { #either no entries at all to begin with or a perfect multiple of 300 $update->sign_tsig("xcat_key",$ctx->{privkey}); my $reply = $resolver->send($update); } } } sub find_nameserver_for_dns { my $ctx = shift; my $zone = shift; my $node = $ctx->{currnode}; my $ip = $ctx->{currip}; my $rname = $ctx->{currrevname}; my $name = $ctx->{currname}; unless ($name =~ /\.\z/) { $name .= '.' } my @rrcontent = ( "$name IN A $ip" ); foreach (keys %{$ctx->{nodeips}->{$node}}) { unless ($_ eq $ip) { push @rrcontent,"$name IN A $_"; } } if ($zone =~ /IN-ADDR.ARPA/) { #reverse style @rrcontent = ("$rname IN PTR $name"); } while ($zone) { unless (defined $ctx->{nsmap}->{$zone}) { #ok, we already thought about this zone and made a decision if ($zone =~ /^\.*192.IN-ADDR.ARPA\.*/ or $zone =~ /^\.*172.IN-ADDR.ARPA\.*/ or $zone =~ /127.IN-ADDR.ARPA\.*/ or $zone =~ /^\.*IN-ADDR.ARPA\.*/ or $zone =~ /^\.*ARPA\.*/) { $ctx->{nsmap}->{$zone} = 0; #ignore zones that are likely to appear, but probably not ours } else { my $reply = $ctx->{resolver}->query($zone,'NS'); if ($reply) { foreach my $record ($reply->answer) { if ( $record->nsdname =~ /blackhole.*\.iana\.org/) { $ctx->{nsmap}->{$zone} = 0; } else { $ctx->{nsmap}->{$zone} = $record->nsdname; } } } else { $ctx->{nsmap}->{$zone} = 0; } } } if ($ctx->{nsmap}->{$zone}) { #we have a nameserver for this zone, therefore this zone is one to update push @{$ctx->{updatesbyzone}->{$zone}},@rrcontent; last; } else { #we have it defined, but zero, means search higher domains. Possible to shortcut further by pointing to the right domain, maybe later if ($zone !~ /\./) { sendmsg([1,"Unable to find reverse zone to hold $node"],$node); last; } $zone =~ s/^[^\.]*\.//; #strip all up to and including first dot unless ($zone) { sendmsg([1,"Unable to find zone to hold $node"],$node); last; } } } } sub sendmsg { # my $callback = $output_handler; my $text = shift; my $node = shift; my $descr; my $rc; if (ref $text eq 'HASH') { die "not right now"; } elsif (ref $text eq 'ARRAY') { $rc = $text->[0]; $text = $text->[1]; } if ($text =~ /:/) { ($descr,$text) = split /:/,$text,2; } $text =~ s/^ *//; $text =~ s/ *$//; my $msg; my $curptr; if ($node) { $msg->{node}=[{name => [$node]}]; $curptr=$msg->{node}->[0]; } else { $msg = {}; $curptr = $msg; } if ($rc) { $curptr->{errorcode}=[$rc]; $curptr->{error}=[$text]; $curptr=$curptr->{error}->[0]; } else { $curptr->{data}=[{contents=>[$text]}]; $curptr=$curptr->{data}->[0]; if ($descr) { $curptr->{desc}=[$descr]; } } # print $outfd freeze([$msg]); # print $outfd "\nENDOFFREEZE6sK4ci\n"; # yield; # waitforack($outfd); $callback->($msg); } sub genpassword { #Generate a pseudo-random password of specified length my $length = shift; my $password = ''; my $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890'; srand; #have to reseed, rand is not rand otherwise while (length($password) < $length) { $password .= substr($characters, int(rand 63), 1); } return $password; } 1;