package xCAT_plugin::ddns;
use strict;
use Getopt::Long;
use Net::DNS;
use File::Path;
use xCAT::Table;
use Sys::Hostname;
use xCAT::TableUtils;
use xCAT::NetworkUtils qw/getipaddr/;
use xCAT::Utils;
use Math::BigInt;
use MIME::Base64;
use xCAT::SvrUtils;
use Socket;
use Fcntl qw/:flock/;
use Data::Dumper;

# This is a rewrite of DNS management using nsupdate rather than 
#	direct zone mangling

my $callback;
my $distro = xCAT::Utils->osver();

my $service="named";

# is this ubuntu ?
if ( $distro =~ /ubuntu.*/i || $distro =~ /debian.*/i ){
    $service = "bind9";	
}

sub handled_commands
{
    my @entries =  xCAT::TableUtils->get_site_attribute("dnshandler");
    my $site_entry = $entries[0];
    unless ( defined($site_entry)) {
        return {"makedns" => "ddns"};
    }

    return {"makedns" => "site:dnshandler"};
}
sub getzonesfornet {
    my $netent = shift;
    my $net = $netent->{net};
    my $mask = $netent->{mask};
    my @zones = ();
    if ($netent->{ddnsdomain}) {
        push @zones,$netent->{ddnsdomain};
    }
    if ($net =~ /:/) {#ipv6, for now do the simple stuff under the assumption we won't have a mask indivisible by 4
        $net =~ s/\/(.*)//;
        my $maskbits=$1;
        if ($maskbits%4) {
            die "Not supporting having a mask like $mask on an ipv6 network like $net";
        }
        my $netnum= getipaddr($net,GetNumber=>1);
        unless ($netnum) { return (); }
        $netnum->brsft(128-$maskbits);
        my $prefix=$netnum->as_hex();
        my $nibbs=$maskbits/4;
        $prefix =~ s/^0x//;
        my $rev;
        foreach (reverse(split //,$prefix)) {
            $rev .= $_.".";
            $nibbs--;
        }
        while ($nibbs) { 
            $rev .= "0.";
            $nibbs--;
        }
        $rev.="ip6.arpa.";
        push @zones,$rev;
        return @zones;
    }
    #return all in-addr reverse zones for a given mask and net
    #for class a,b,c, the answer is easy
    #for classless, identify the partial byte, do $netbyte | (0xff&~$maskbyte) to get the highest value
    #return sequence from $net to value calculated above
    #since old bind.pm only went as far as class c, we will carry that over for now (more people with smaller than class c complained
    #and none hit the theoretical conflict.  FYI, the 'official' method in RFC 2317 seems cumbersome, but maybe one day it makes sense
    #since this is dhcpv4 for now, we'll use the inet_aton, ntop functions to generate the answers (dhcpv6 omapi would be nice...)
    my $netn = inet_aton($net);
    my $maskn = inet_aton($mask);
    unless ($netn and $mask) { return (); }
    my $netnum = unpack('N',$netn);
    my $masknum = unpack('N',$maskn);
    if ($masknum >= 0xffffff00) { #treat all netmasks higher than 255.255.255.0 as class C
        $netnum = $netnum & 0xffffff00;
        $netn = pack('N',$netnum);
        $net = inet_ntoa($netn);
        $net =~ s/\.[^\.]*$//;
        return (join('.',reverse(split('\.',$net))).'.IN-ADDR.ARPA.');
    } elsif ($masknum > 0xffff0000) { #(/17) to /23
        my $tempnumber = ($netnum >> 8);
        $masknum = $masknum >> 8;
        my $highnet = $tempnumber | (0xffffff & ~$masknum);
        foreach ($tempnumber..$highnet) {
            $netnum = $_ << 8;
            $net = inet_ntoa(pack('N',$netnum));
            $net =~ s/\.[^\.]*$//;
            push @zones,join('.',reverse(split('\.',$net))).'.IN-ADDR.ARPA.';
        }
        return @zones;
    } elsif ($masknum > 0xff000000) { # (/9) to class b /16, could have made it more flexible, for for only two cases, not worth in
        my $tempnumber = ($netnum >> 16); #the last two bytes are insignificant, shift them off to make math easier
        $masknum = $masknum >> 16;
        my $highnet = $tempnumber | (0xffff & ~$masknum);
        foreach ($tempnumber..$highnet) {
            $netnum = $_ << 16; #convert back to the real network value
            $net = inet_ntoa(pack('N',$netnum));
            $net =~ s/\.[^\.]*$//;
            $net =~ s/\.[^\.]*$//;
            push @zones,join('.',reverse(split('\.',$net))).'.IN-ADDR.ARPA.';
        }
        return @zones;
    } else { #class a (theoretically larger, but those shouldn't exist)
        my $tempnumber = ($netnum >> 24); #the last two bytes are insignificant, shift them off to make math easier
        $masknum = $masknum >> 24;
        my $highnet = $tempnumber | (0xff & ~$masknum);
        foreach ($tempnumber..$highnet) {
            $netnum = $_ << 24; #convert back to the real network value
            $net = inet_ntoa(pack('N',$netnum));
            $net =~ s/\.[^\.]*$//;
            $net =~ s/\.[^\.]*$//;
            $net =~ s/\.[^\.]*$//;
            push @zones,join('.',reverse(split('\.',$net))).'.IN-ADDR.ARPA.';
        }
        return @zones;
    }
}
sub get_reverse_zones_for_entity {
    my $ctx = shift;
    my $node = shift;
    my $net;
    if (($node =~ /loopback/) || ($node =~ /localhost/))
    {
        # do not use DNS to resolve localhsot
        return;
    }

    if ($ctx->{hoststab} and $ctx->{hoststab}->{$node} and $ctx->{hoststab}->{$node}->[0]->{ip}) {
        $node = $ctx->{hoststab}->{$node}->[0]->{ip};
    }
    my @tvars=getipaddr($node,GetNumber=>1,GetAllAddresses=>1);
    my $tvar;
    my @revs;
    foreach $tvar (@tvars) {
        foreach my $net (keys %{$ctx->{nets}}) {
            if ($ctx->{nets}->{$net}->{netn} == ($tvar & $ctx->{nets}->{$net}->{mask})) {
                if ($net =~ /\./) { #IPv4/IN-ADDR.ARPA case.
                    my $maskstr = unpack("B32",pack("N",$ctx->{nets}->{$net}->{mask}));
                    my $maskcount = ($maskstr =~ tr/1//);
                    if ($maskcount >= 24)
                    {
                        $maskcount-=($maskcount%8);  #e.g. treat the 27bit netmask as 24bit
                    }
                    else
                    {
                        $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.';
                    push @revs,$rev;
                } elsif ($net =~ /:/) {#v6/ip6.arpa case
                    $net =~ /\/(.*)/;
                    my $maskbits = $1;
                    unless ($maskbits and (($maskbits%4)==0)) {
                        die "Never expected this, $net should have had CIDR / notation... and the mask should be a factor of 4, if not, need work..."
                    }
                    my $netnum = Math::BigInt->new($ctx->{nets}->{$net}->{netn});
                    $netnum->brsft(128-$maskbits);
                    my $prefix=$netnum->as_hex();
                    my $nibbs=$maskbits/4;
                    $prefix =~ s/^0x//;
                    my $rev;
                    foreach (reverse(split //,$prefix)) {
                        $rev .= $_.".";
                        $nibbs--;
                    }
                    while ($nibbs) { 
                        $rev .= "0.";
                        $nibbs--;
                    }
                    $rev.="ip6.arpa.";
                    push @revs,$rev;
                }
            }
        }
    }
    return @revs;
}

sub process_request {
    my $request = shift;
    $callback = shift;
    my $oldmask = umask(0007);
    my $ctx = {};
    my @nodes=();
    my $hadargs=0;
    my $allnodes;
    my $zapfiles;
    my $help;
    my $deletemode=0;
    my $external=0;
    my $slave=0;
    my $VERBOSE;

    # Since the mandatory rpm perl-Net-DNS for makedns on sles12 (perl-Net-DNS-0.73-1.28)  has a bug,
    # user has to update it to a newer version
    my @rpminfo = `rpm -qi perl-Net-DNS`;
    my ($matchedver, $matchedrel);
    foreach (@rpminfo) {
        if (/Version\s*:\s*0.73/i) {
            $matchedver = 1;
        } elsif (/Release\s*:\s*1.28/i) {
            $matchedrel = 1;
        }
    }
    if ($matchedver && $matchedrel) {
        xCAT::MsgUtils->message("E", {error => ["The necessary rpm perl-Net-DNS-0.73-1.28 needs be updated to a higher version for makedns to function. You can get a workable version perl-Net-DNS-0.80-1.x86_64.rpm from xCAT Dependency Repository. e.g. For sles12: zypper install perl-Net-DNS-0.80-1.<arch>"], errorcode => [1]}, $callback);
        return;
    }        

    if ($request->{arg}) {
        $hadargs=1;
        @ARGV=@{$request->{arg}};

        Getopt::Long::Configure("no_pass_through");
        Getopt::Long::Configure("bundling");
        if (!GetOptions(
            'a|all' => \$allnodes,
            'n|new' => \$zapfiles,
            'd|delete' => \$deletemode,
            'e|external' => \$external,
            's|slave' => \$slave,
            'V|verbose'     => \$VERBOSE,
            'h|help' => \$help,
            )) {
            #xCAT::SvrUtils::sendmsg([1,"TODO: makedns Usage message"], $callback);
            makedns_usage($callback);
            umask($oldmask);
            return;
        }
    }
    if (defined($VERBOSE)) {
        $::VERBOSE=$VERBOSE;
    } else {
        undef $::VERBOSE;
    }

    if ($::XCATSITEVALS{externaldns}) {
        $external=1;
    }

    if ($help)
    {
        makedns_usage($callback);
        umask($oldmask);
        return;
    }

    if ($deletemode && (!$request->{node}->[0]))
    {
        makedns_usage($callback);
        umask($oldmask);
        return;
    }
    
    $ctx->{deletemode}=$deletemode;

    # check for site.domain     
    my @entries =  xCAT::TableUtils->get_site_attribute("domain");
    my $site_entry = $entries[0];
    unless ( defined($site_entry)) {
        xCAT::SvrUtils::sendmsg([1,"domain not defined in site table"], $callback);
        umask($oldmask);
        return;
    }
    $ctx->{domain} = $site_entry;
    if ($::VERBOSE)
    {
        my $rsp;
        push @{$rsp->{data}},
            "domain name = $site_entry";
        xCAT::MsgUtils->message("I", $rsp, $callback);
    }

    if($external) #need to check if /etc/resolv.conf existing
    {
        my $resolv = "/etc/resolv.conf";
        my $cmd = "egrep '^nameserver|^search' $resolv";
        my $acmd = "egrep '^nameserver|^domain' $resolv";

        if (xCAT::Utils->isAIX())
        {
            $cmd = $acmd;
        }
        

        my @output=xCAT::Utils->runcmd($cmd, 0);
        if ($::VERBOSE)
        {
            my $rsp;
            my $outp = join(', ', @output);
            push @{$rsp->{data}},
                "output from /etc/resolv.conf:  $outp";
            xCAT::MsgUtils->message("I", $rsp, $callback);
        }

        if ($::RUNCMD_RC != 0)
        {
            xCAT::SvrUtils::sendmsg([1,"You are using -e flag to update DNS records to an external DNS server, please ensure /etc/resolv.conf existing and pointed to this external DNS server."], $callback);
            umask($oldmask);
            return;
        }
   }
   
   # check for selinux disabled
    my $rc=xCAT::Utils->isSELINUX();
    if ($rc == 0)
    {
        xCAT::SvrUtils::sendmsg([0,"Warning:SELINUX is not disabled. The makedns command will not be able to generate a complete DNS setup. Disable SELINUX and run the command again."], $callback);

    }
    my @entries =  xCAT::TableUtils->get_site_attribute("nameservers");
    my $sitens = $entries[0];
    unless ( defined($site_entry)) {
        xCAT::SvrUtils::sendmsg([1,"nameservers not defined in site table"], $callback);
        umask($oldmask);
        return;
    }

    if ($::VERBOSE)
    {
        my $rsp;
        push @{$rsp->{data}},
            "nameservers = $sitens";
        xCAT::MsgUtils->message("I", $rsp, $callback);
    }
     
    my $networkstab = xCAT::Table->new('networks',-create=>0);
    unless ($networkstab) { xCAT::SvrUtils::sendmsg([1,'Unable to enumerate networks, try to run makenetworks'], $callback); }

    my @networks = $networkstab->getAllAttribs('net','mask','ddnsdomain','domain','nameservers');
    # exclude the net if it is using an external dns server.
    foreach my $net (@networks)
    {
        if ($net and $net->{nameservers})
        {
            my $valid = 0;
            my $myip = xCAT::NetworkUtils->my_ip_facing($net->{net});
            foreach (split /,/, $net->{nameservers})
            {
                chomp $_;
                if (($_ eq $myip) || ($_ eq '<xcatmaster>') || ($_ eq $sitens))
                {
                    $valid += 1;
                }
            }
            unless ($valid > 0)
            {
                $net = undef;
            }
        }
    }

#    if ($request->{node}) { #we have a noderange to process
#        @nodes = @{$request->{node}};
    #} elsif ($allnodes) {
    if ($allnodes) {
        #read all nodelist specified nodes
    } else { 
        if (not $request->{node} and $deletemode) {
            #when this was permitted, it really ruined peoples' days
            xCAT::SvrUtils::sendmsg([1,"makedns -d without noderange or -a is not supported"],$callback); 
            umask($oldmask);
            return;
        }
        my @contents;
        my $domain;
        if ($request->{node}) { #leverage makehosts code to flesh out the options
        require xCAT_plugin::hosts;
        my @content1;
        my @content2;
        xCAT_plugin::hosts::add_hosts_content(nodelist=>$request->{node},callback=>$callback,hostsref=>\@content1);
        xCAT_plugin::hosts::donics(nodes=>$request->{node},callback=>$callback,hostsref=>\@content2);
                @contents = (@content1, @content2);
        } else {
        #legacy behavior, read from /etc/hosts
        my $hostsfile;
        open($hostsfile,"<","/etc/hosts");
        flock($hostsfile,LOCK_SH);
        @contents = <$hostsfile>;
        flock($hostsfile,LOCK_UN);
        close($hostsfile);
        }
        my $addr;
        my $name;
        my $canonical;
        my $aliasstr;
        my @aliases;
        my $names;
        my @hosts;
        my %nodehash;
    
        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+$/ and $addr !~ /^[abcdef0123456789:]+$/) {
            xCAT::SvrUtils::sendmsg(":Ignoring line $_ in /etc/hosts, address seems malformed.", $callback);
            next;
        }
        unless ($names =~ /^[a-z0-9\. \t\n-]+$/i) {
            xCAT::SvrUtils::sendmsg(":Ignoring line $_ in /etc/hosts, names  $names contain invalid characters (valid characters include a through z, numbers and the '-', but not '_'", $callback);
            next;
        }
        
        my ($host, $ip) = xCAT::NetworkUtils->gethostnameandip($addr);
        push @hosts, $host;
        $nodehash{$addr}{names}=$names;
        $nodehash{$addr}{host}=$host;
        }

        # get the domains for each node - one call for all nodes in hosts file
        my $nd = xCAT::NetworkUtils->getNodeDomains(\@hosts);
        my %nodedomains = %$nd;
        
        foreach my $n (keys %nodehash) {
            $addr=$n;
            $names=$nodehash{$n}{names};
            # - need domain for this node
            my $host = $nodehash{$n}{host};
            $domain=$nodedomains{$host};
            # remove the first . at domain name since it's not accepted by high dns parser
            if ($domain =~ /^\./) { $domain =~ s/^\.//;; } 

            ($canonical,$aliasstr)  = split /[ \t]+/,$names,2;
            if ($aliasstr) {
                @aliases= split /[ \t]+/,$aliasstr;
            } else {
                @aliases = ();
            }
            my %names = ();
            my $node = $canonical;

            xCAT::SvrUtils::sendmsg(":Handling $node in /etc/hosts.", $callback);
            
            unless ($canonical =~ /$domain/) {
                $canonical.=".".$domain;
            }
            # for only the sake of comparison, ensure consistant dot suffix
            unless ($canonical =~ /\.\z/) { $canonical .= '.' }
            foreach my $alias (@aliases) {
                unless ($alias =~ /$domain/) {
                    $alias .= ".".$domain;
                }
                unless ($alias =~ /\.\z/) {
                    $alias .= '.';
                }
                if ($alias eq $canonical) {
                    next;
                }
                # remember alias for CNAM records later
                $ctx->{aliases}->{$node}->{$alias}=1;
            }
            
            # exclude the nodes not belong to any nets defined in networks table
            # 	because only the nets defined in networks table will be add 
            #	zones later.
            my $found = 0;
            foreach (@networks)
            {
                if(xCAT::NetworkUtils->ishostinsubnet($addr, $_->{mask}, $_->{net}))
                {
                    $found = 1;
                }
            }

            if ($found)
            {
                push @nodes,$node;
                $ctx->{nodeips}->{$node}->{$addr}=1;
            }
            else
            {
                unless ($node =~ /localhost/)
                {
                    xCAT::SvrUtils::sendmsg(":Ignoring host $node, it does not belong to any nets defined in networks table or the net it belongs to is configured to use an external nameserver.", $callback);
                }    
            }
        }
    }

    my $hoststab = xCAT::Table->new('hosts',-create=>0);
    if ($hoststab) {
        $ctx->{hoststab} = $hoststab->getNodesAttribs(\@nodes,['ip']);
    }
    $ctx->{nodes} = \@nodes;

    foreach (@networks) {
        my $maskn;
        if ($_->{mask}) { #better be IPv4, we only do CIDR for v6, use the v4/v6 agnostic just in case
            $maskn = getipaddr($_->{mask},GetNumber=>1); #pack("N",inet_aton($_->{mask}));
        } elsif ($_->{net} =~ /\/(.*)/) { #CIDR
            my $maskbits=$1;
            my $numbits;
            if ($_->{net} =~ /:/) { #v6
                $numbits=128;
            } elsif ($_->{net} =~ /\./) {
                $numbits=32;
            } else {
                umask($oldmask);
                die "Network ".$_->{net}." appears to be malformed in networks table";
            }
            $maskn = Math::BigInt->new("0b".("1"x$maskbits).("0"x($numbits-$maskbits)));
        }
        $ctx->{nets}->{$_->{net}}->{mask} = $maskn;

        my $net = $_->{net};
        $net =~ s/\/.*//;
        $ctx->{nets}->{$_->{net}}->{netn} = getipaddr($net,GetNumber=>1);
        my $currzone;
        foreach $currzone (getzonesfornet($_)) {
            $ctx->{zonestotouch}->{$currzone} = 1;
            if ($::VERBOSE)
            {
                my $rsp;
                push @{$rsp->{data}},
                    "zone info for this $net: $currzone";
                xCAT::MsgUtils->message("I", $rsp, $callback);
            }
        }
    }
    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

    my @entries =  xCAT::TableUtils->get_site_attribute("forwarders");
    my $site_entry = $entries[0];
    if ( defined($site_entry)) {
        my @forwarders = split /[ ,]/,$site_entry;
        $ctx->{forwarders}=\@forwarders;
    }

    my @slave_ips;
    my $dns_slaves = get_dns_slave();
    if (scalar @$dns_slaves) {
        foreach my $slave_hn (@$dns_slaves) {
            my $slave_ip = xCAT::NetworkUtils->getipaddr($slave_hn);
            push @slave_ips, $slave_ip;
        }
        $ctx->{slaves}=\@slave_ips;
    }

    $ctx->{domain} =~ s/^\.//; # remove . if it's the first char of domain name
    $ctx->{zonestotouch}->{$ctx->{domain}}=1;
    foreach (@networks) {
        if ($_->{domain}) {
            $_->{domain} =~ s/^\.//; # remove . if it's the first char of domain name
            $ctx->{zonestotouch}->{$_->{domain}}=1;
        }
    }

    # get the listen on port for the DNS server from site.dnsinterfaces
    my @dnsifinsite =  xCAT::TableUtils->get_site_attribute("dnsinterfaces");
    if (@dnsifinsite)
    #syntax should be like host|ifname1,ifname2;host2|ifname3,ifname2 etc or simply ifname,ifname2
    {
        my $dnsinterfaces = $dnsifinsite[0];
        my $listenonifs;
        foreach my $dnsif (split /;/,$dnsinterfaces) {
            if ($dnsif =~ /\|/) {  # there's host in the string
                my ($hosts,$dnsif) = split /\|/,$dnsif;
                if (! xCAT::NetworkUtils->thishostisnot($hosts)) {
                   $listenonifs=$dnsif;
                } else {
                    # this host string might be a xcat group, try to test each node in the group
                    foreach my $host (noderange($hosts)) {
                       unless (xCAT::NetworkUtils->thishostisnot($host)) {
                           $listenonifs=$dnsif;
                           last;
                       }
                    }
                }
            } else {
                $listenonifs = $dnsif;
            }

            # get the ip for each interface and set it to $ctx->{dnslistenonifs}
            if ($listenonifs) {
                $listenonifs = "lo,".$listenonifs;
                # get the ip address for each interface
                my (@listenipv4, @listenipv6);
                for my $if (split /,/, $listenonifs) {
                    my @ifaddrs = `ip addr show $if`;
                    foreach (@ifaddrs) {
                        if (/^\s*inet\s+([^ ]*)/) {
                            my $ip = $1; 
                            $ip =~ s/\/.*//;
                            push  @listenipv4, $ip;
                        } elsif (/^\s*inet6\s+([^ ]*)/) {
                            my $ip = $1;
                            $ip =~ s/\/.*//;
                            push  @listenipv6, $ip;
                        }
                    }
                }
                if (@listenipv4) {
                    $ctx->{dnslistenonifs}->{ipv4} = \@listenipv4;
                }
                if (@listenipv6) {
                    $ctx->{dnslistenonifs}->{ipv6} = \@listenipv6;
                }
                last;
            }
        }
    }
    
    xCAT::SvrUtils::sendmsg("Getting reverse zones, this may take several minutes for a large cluster.", $callback);
    
    foreach (@nodes) {
        my @revzones =  get_reverse_zones_for_entity($ctx,$_);;
        unless (@revzones) 
        { 
            if ($::VERBOSE)
            {
                my $rsp;
                push @{$rsp->{data}},
                    "No reverse zones for $_ ";
                xCAT::MsgUtils->message("I", $rsp, $callback);
            } 
            next; 
        }
        $ctx->{revzones}->{$_} = \@revzones;
        foreach (@revzones) {
            $ctx->{zonestotouch}->{$_}=1;
        }
    }
    xCAT::SvrUtils::sendmsg("Completed getting reverse zones.", $callback);
    
    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 
        #   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
        @entries =  xCAT::TableUtils->get_site_attribute("directoryprovider");
        $site_entry = $entries[0];
        if ( defined($site_entry) and $site_entry eq 'activedirectory') {
            @entries =  xCAT::TableUtils->get_site_attribute("directoryservers");
            $site_entry = $entries[0];
            if ( defined($site_entry)) {
                my @dservers = split /[ ,]/,$site_entry;
                $ctx->{adservers} = \@dservers;
                
                ############################
                # - should this include all domains?
                # - multi-domains not supported with activedirectory
                #  	- TODO in future release
                ###################
                
                $ctx->{adzones} = {
                    "_msdcs.". $ctx->{domain} => 1,
                    "_sites.". $ctx->{domain} => 1,
                    "_tcp.". $ctx->{domain} => 1,
                    "_udp.". $ctx->{domain} => 1,
                };
            }
        }
    
        @entries =  xCAT::TableUtils->get_site_attribute("dnsupdaters");
        $site_entry = $entries[0];
        if ( defined($site_entry) ) {
            my @nservers = split /[ ,]/,$site_entry;
            $ctx->{dnsupdaters} = \@nservers;
        }
        unless ($external) { # only generate the named.conf and zone files for xCAT dns when NOT using external dns
            if ($zapfiles || $slave) { #here, we unlink all the existing files to start fresh
                if ($::VERBOSE)
                {
                    my $rsp;
                    push @{$rsp->{data}},
                        "Stop named service";
                    xCAT::MsgUtils->message("I", $rsp, $callback);
                }
 
                if (xCAT::Utils->isAIX())
                {
                    system("/usr/bin/stopsrc -s $service");
                }
                else
                {
                    #system("service $service stop"); #named may otherwise hold on to stale journal filehandles
                    xCAT::Utils->stopservice("named");
                }
                my $conf = get_conf();
                unlink $conf;
                my $DBDir = get_dbdir();
                if ($::VERBOSE)
                {
                    my $rsp;
                    push @{$rsp->{data}},
                        "get_dbdir: $DBDir";
                    xCAT::MsgUtils->message("I", $rsp, $callback);
                }

                foreach (<$DBDir/db.*>) {
                    unlink $_;
                }
            }
            #We manipulate local namedconf
            $ctx->{dbdir} = get_dbdir();
            $ctx->{zonesdir} = get_zonesdir();
            chmod 0775, $ctx->{dbdir}; # assure dynamic dns can actually execute against the directory
            if ($::VERBOSE)
            {
                my $rsp;
                push @{$rsp->{data}},
                    "Update Named Conf dir $ctx->{dbdir} $ctx->{zonesdir}";
                xCAT::MsgUtils->message("I", $rsp, $callback);
            }
 
            update_namedconf($ctx, $slave); 
            
            unless ($slave)
            {
                update_zones($ctx);
            }
      
            if ($ctx->{restartneeded}) {
                xCAT::SvrUtils::sendmsg("Restarting $service", $callback);
    
                    if (xCAT::Utils->isAIX())
                    {
                        #try to stop named
                        my $cmd = "/usr/bin/stopsrc -s $service";
                        my @output=xCAT::Utils->runcmd($cmd, 0);
    
                        $cmd = "/usr/bin/startsrc -s $service";
                        @output=xCAT::Utils->runcmd($cmd, 0);
                        my $outp = join('', @output);
                        if ($::RUNCMD_RC != 0)
                        {
                            my $rsp = {};
                            $rsp->{data}->[0] = "Command failed: $cmd. Error message: $outp.\n";
                            xCAT::MsgUtils->message("E", $rsp, $callback);
                            return;
                        }
                    }
                    else
                    {
                        #my $cmd = "service $service stop";
                        #my @output=xCAT::Utils->runcmd($cmd, 0);
                        #my $outp = join('', @output);
                        #if ($::RUNCMD_RC != 0)
                        #{
                        #    my $rsp = {};
                        #    $rsp->{data}->[0] = "Command failed: $cmd. Error message: $outp.\n";
                        #    xCAT::MsgUtils->message("E", $rsp, $callback);
                        #    return;
                        #}
    
                        #$cmd = "service $service start";
                        #@output=xCAT::Utils->runcmd($cmd, 0);
                        #$outp = join('', @output);
                        #if ($::RUNCMD_RC != 0)
                        #{
                        #    my $rsp = {};
                        #    $rsp->{data}->[0] = "Command failed: $cmd. Error message: $outp.\n";
                        #    xCAT::MsgUtils->message("E", $rsp, $callback);
                        #    return;
                        #}
                        my $retcode=xCAT::Utils->restartservice("named");
                        if($retcode!=0){
                            my $rsp = {};
                            $rsp->{data}->[0] = "failed to start named.\n";
                            xCAT::MsgUtils->message("E", $rsp, $callback);
                            return;
                        }
                    }
    
                xCAT::SvrUtils::sendmsg("Restarting named complete", $callback);
            }
        }
    } else {
        unless ($ctx->{privkey}) {
            xCAT::SvrUtils::sendmsg([1,"Unable to update DNS due to lack of credentials in passwd to communicate with remote server"], $callback);
        }
    }
    
    if ($slave)
    {
        return;
    }

    # check if named is active before update dns records.
    unless ($external) { # only start xCAT local dns when NOT using external dns
        if (xCAT::Utils->isAIX())
        {
            my $cmd = "/usr/bin/lssrc -s $service |grep active";
            my @output=xCAT::Utils->runcmd($cmd, 0);
            if ($::RUNCMD_RC != 0)
            {
                $cmd = "/usr/bin/startsrc -s $service";
                @output=xCAT::Utils->runcmd($cmd, 0);
                my $outp = join('', @output);
                if ($::RUNCMD_RC != 0)
                {
                    my $rsp = {};
                    $rsp->{data}->[0] = "Command failed: $cmd. Error message: $outp.\n";
                    xCAT::MsgUtils->message("E", $rsp, $callback);
                    return;
                }
            }
        }
        else
        {
            #my $cmd = "service $service status|grep running";
            #my @output=xCAT::Utils->runcmd($cmd, 0);
            #if ($::RUNCMD_RC != 0)
            #{
            #    $cmd = "service $service start";
            #    @output=xCAT::Utils->runcmd($cmd, 0);
            #    my $outp = join('', @output);
            #    if ($::RUNCMD_RC != 0)
            #    {
            #        my $rsp = {};
            #        $rsp->{data}->[0] = "Command failed: $cmd. Error message: $outp.\n";
            #        xCAT::MsgUtils->message("E", $rsp, $callback);
            #        return;
            #    }
            #}
            my $retcode=xCAT::Utils->startservice("named");
            if($retcode!=0)
            {
                    my $rsp = {};
                    $rsp->{data}->[0] = "failed to start named.\n";
                    xCAT::MsgUtils->message("E", $rsp, $callback);
                    return;
                
            }
        }
    }
        
    #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....
    if ($external)
    {
        # based on /etc/resolv.conf
        $ctx->{resolver} = Net::DNS::Resolver->new(); 
    }
    else
    {
        # default to localhost
        $ctx->{resolver} = Net::DNS::Resolver->new(nameservers=>['127.0.0.1']); 
    }

    my $ret = add_or_delete_records($ctx);
    unless($ret) {
        xCAT::SvrUtils::sendmsg("DNS setup is completed", $callback);
    }

    umask($oldmask);
}

sub get_zonesdir {
    my $ZonesDir = get_dbdir();

    my @entries =  xCAT::TableUtils->get_site_attribute("bindzones");
    my $site_entry = $entries[0];

    if ( defined($site_entry) ) {
        $ZonesDir= $site_entry;
    }

    return "$ZonesDir";
}

sub get_conf {
    my $conf="/etc/named.conf";

    # is this ubuntu ?
    if ( $distro =~ /ubuntu.*/i || $distro =~ /debian.*/i ){
        $conf="/etc/bind/named.conf";
    }

    my @entries =  xCAT::TableUtils->get_site_attribute("bindconf");
    my $site_entry = $entries[0];

    if ( defined($site_entry) ) {
        $conf= $site_entry;
    }

    return "$conf";
}

sub get_dbdir {
    my $DBDir;

    my @entries =  xCAT::TableUtils->get_site_attribute("binddir");
    my $site_entry = $entries[0];
    if ( defined($site_entry) ) {
        $DBDir = $site_entry;
    }

    if ( -d "$DBDir" ) {
        return "$DBDir"
    } elsif (-d "/var/named") {
        return "/var/named/";
    } elsif (-d "/var/lib/named") {
        # Temp fix for bugzilla 73119
        chown(scalar(getpwnam('root')),scalar(getgrnam('named')),"/var/lib/named");
        return "/var/lib/named/";
    } 
    elsif (-d "/var/lib/bind") {
        return "/var/lib/bind/";
    } 
    else {
        mkpath "/var/named/";
        chown(scalar(getpwnam('named')),scalar(getgrnam('named')),"/var/named");
        return "/var/named/";
    }
}

sub isvalidip {
    #inet_pton/ntop good for ensuring an ip looks like an ip? or do string compare manually?
    #for now, do string analysis, one problem with pton/ntop is that 010.1.1.1 would look diff from 10.1.1.1)
    my $candidate = shift;
    if ($candidate =~ /^(\d+)\.(\d+)\.(\d+).(\d+)\z/) {
        return (
            $1 >= 0 and $1 <= 255 and
            $2 >= 0 and $2 <= 255 and
            $3 >= 0 and $3 <= 255 and
            $4 >= 0 and $4 <= 255
            );
    }
}
sub update_zones {
    my $ctx = shift;
    my $currzone;
    my $dbdir = $ctx->{dbdir};
    my $name = hostname;
    my $node = $name;

    # get the domain for the node - which is the local hostname
    my ($host, $nip) = xCAT::NetworkUtils->gethostnameandip($node);
    my @hosts;
    push (@hosts, $host);
    my $nd = xCAT::NetworkUtils->getNodeDomains(\@hosts);
    my %nodedomains = %$nd;
    my $domain = $nodedomains{$host};

    xCAT::SvrUtils::sendmsg("Updating zones.", $callback);

    if ($domain =~ /^\./) { # remove . if it's the first char of domain name
        $domain =~ s/^\.//;
    }
    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};
        unless (isvalidip($ip)) {
            xCAT::SvrUtils::sendmsg([1,"The hosts table entry for $node indicates $ip as an ip address, which is not a valid address"], $callback);
            next;
        }
    } 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";
            xCAT::SvrUtils::sendmsg([1,"Unable to find an IP for $node in hosts table or via system lookup (i.e. /etc/hosts"], $callback);
            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;
        #If needed, the below, but it was a fairly painfully restricted paradigm for zonefile names...
        #} elsif (not $zonefilename =~ /_/) {
        #    $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);
            if ( $distro =~ /ubuntu.*/i || $distro =~ /debian.*/i ){
                chown(scalar(getpwnam('root')),scalar(getgrnam('bind')),$dbdir."/db.$zonefilename");
            }
            else{
                chown(scalar(getpwnam('named')),scalar(getgrnam('named')),$dbdir."/db.$zonefilename");
            }
            $ctx->{restartneeded}=1;
        }
    }
    xCAT::SvrUtils::sendmsg("Completed updating zones.", $callback);
}

sub update_namedconf {
    my $ctx = shift;
    my $slave = shift;
    my $namedlocation = get_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<scalar(@currnamed);$i++) {
            my $line = $currnamed[$i];
            if ($line =~ /^options +\{/) {
                $gotoptions=1;
                my $skip=0;
                do {
                    #push @newnamed,"\t\t//listen-on-v6 { any; };\n";
                    if ($ctx->{forwarders} and $line =~ /forwarders {/) {
                        push @newnamed,"\tforwarders \{\n";
                        $skip=1;
                        foreach (@{$ctx->{forwarders}}) {
                            push  @newnamed,"\t\t".$_.";\n";
                        }
                        push @newnamed,"\t};\n";
                    } elsif ($ctx->{slaves} and $line =~ /allow-transfer {/) {
                        push @newnamed,"\tallow-transfer \{\n";
                        $skip=1;
                        foreach (@{$ctx->{slaves}}) {
                            push  @newnamed,"\t\t".$_.";\n";
                        }
                        push @newnamed,"\t};\n";
                    } elsif ($ctx->{slaves} and $line =~ /also-notify {/) {
                        push @newnamed,"\talso-notify \{\n";
                        $skip=1;
                        foreach (@{$ctx->{slaves}}) {
                            push  @newnamed,"\t\t".$_.";\n";
                        }
                        push @newnamed,"\t};\n";                    
                    } elsif (defined($ctx->{dnslistenonifs}) and defined($ctx->{dnslistenonifs}->{ipv4}) and $line =~ /listen-on {/) {
                        push @newnamed,"\tlisten-on \{\n";
                        $skip=1;
                        foreach (@{$ctx->{dnslistenonifs}->{ipv4}}) {
                            push  @newnamed,"\t\t".$_.";\n";
                        }
                        push @newnamed,"\t};\n";                    
                    } elsif (defined($ctx->{dnslistenonifs}) and defined($ctx->{dnslistenonifs}->{ipv6}) and $line =~ /listen-on-v6 {/) {
                        push @newnamed,"\tlisten-on-v6 \{\n";
                        $skip=1;
                        foreach (@{$ctx->{dnslistenonifs}->{ipv6}}) {
                            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";
        unless ($slave && xCAT::Utils->isLinux()) {
           push @newnamed,"\tdirectory \"".$ctx->{zonesdir}."\";\n";
           push @newnamed, "\tallow-recursion { any; };\n";
        }
        #push @newnamed,"\t\t//listen-on-v6 { any; };\n";
        if ($ctx->{forwarders}) {
            push @newnamed,"\tforwarders {\n";
            foreach (@{$ctx->{forwarders}}) {
                push @newnamed,"\t\t$_;\n";
            }
            push @newnamed,"\t};\n";
        }

        if ($slave) {
            push @newnamed,"\tallow-transfer { any; };\n";
        } else {
            if ($ctx->{slaves}) {
                push @newnamed,"\tnotify yes;\n";
                push @newnamed,"\tallow-transfer {\n";
                foreach (@{$ctx->{slaves}}) {
                    push @newnamed,"\t\t$_;\n";
                }
                push @newnamed,"\t};\n";
                push @newnamed,"\talso-notify {\n";
                foreach (@{$ctx->{slaves}}) {
                    push @newnamed,"\t\t$_;\n";
                }            
                push @newnamed,"\t};\n";            
            }
        }
        
        # add the listen-on option
        if (defined($ctx->{dnslistenonifs}) and defined($ctx->{dnslistenonifs}->{ipv4})) {
            push @newnamed, "\tlisten-on \{\n";
            foreach (@{$ctx->{dnslistenonifs}->{ipv4}}) {
                push  @newnamed,"\t\t".$_.";\n";
            }
            push @newnamed,"\t};\n"
        }
        if (defined($ctx->{dnslistenonifs}) and defined($ctx->{dnslistenonifs}->{ipv6})) {
            push @newnamed,"\tlisten-on-v6 \{\n";
            foreach (@{$ctx->{dnslistenonifs}->{ipv6}}) {
                push  @newnamed,"\t\t".$_.";\n";
            }
            push @newnamed,"\t};\n";                    
        }
        
        push @newnamed,"};\n\n";
    }

    unless ($slave) {
        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 $cmd = "grep '^nameserver' /etc/resolv.conf | awk '{print \$2}'";
    my @output = xCAT::Utils->runcmd($cmd, 0);
    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";
        if ($slave) {
            push @newnamed,"\ttype slave;\n";
            push @newnamed,"\tmasters { $output[0]; };\n";
        } else {
           push @newnamed,"\ttype master;\n","\tallow-update {\n","\t\tkey xcat_key;\n","\t};\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,"\tfile \"db.$net\";\n","};\n";

        } else {
            my $zfilename = $zone;
            #$zfilename =~ s/\..*//;
            push @newnamed,"\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";
        if ($slave) {
            push @newnamed,"\ttype slave;\n";
            push @newnamed,"\tmasters { $output[0]; };\n";
        } else {
            push @newnamed,"\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";
    }

    # For AIX, add a hint zone
    if (xCAT::Utils->isAIX())
    {
        unless (grep(/hint/, @newnamed))
        {
            push @newnamed,"zone \"\.\" in {\n","\ttype hint;\n","\tfile \"db\.cache\";\n","};\n\n";
            # Toutch the stub zone file
            system("/usr/bin/touch $ctx->{dbdir}.'/db.cache'");
            $ctx->{restartneeded}=1;
            if($ctx->{forwarders})
            {
                my $dbcachefile = $ctx->{dbdir}.'/db.cache';
                my $cmd = qq~dig @"$ctx->{forwarders}[0]" . ns >> $dbcachefile~;
                my $outref = xCAT::Utils->runcmd("$cmd", 0);
                if ($::RUNCMD_RC != 0)
                {
                    my $rsp = {};
                    $rsp->{data}->[0] = "Failed to run command: $cmd.\n";
                    xCAT::MsgUtils->message("W", $rsp, $callback, 1);
                }
            }
        }
    }

    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);
    if ( $distro =~ /ubuntu.*/i || $distro =~ /debian.*/i ){
        chown (scalar(getpwnam('root')),scalar(getgrnam('bind')),$namedlocation);
    }
    else{
        chown (scalar(getpwnam('root')),scalar(getgrnam('named')),$namedlocation);
    }
}

sub add_or_delete_records {
    my $ctx = shift;

    xCAT::SvrUtils::sendmsg("Updating DNS records, this may take several minutes for a large cluster.", $callback);
    
    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 {
            xCAT::SvrUtils::sendmsg([1,"Unable to find omapi key in passwd table"], $callback);
        }
    }
    my $node;
    my @ips;

    $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

    # get node domains
    my $nd = xCAT::NetworkUtils->getNodeDomains(\@{$ctx->{nodes}});
    my %nodedomains = %{$nd};

    foreach $node (@{$ctx->{nodes}}) {
        my $name = $node;

        if (($name =~ /loopback/) || ($name =~ /localhost/))
        {
            next;
        }

        my $domain = $nodedomains{$node};
        if ($domain =~ /^\./) { $domain =~ s/^\.//; } # remove . if it's the first char of domain name

        unless ($name =~ /$domain/) { $name .= ".".$domain } # $name needs to represent fqdn, but must preserve $node as a nodename for cfg lookup

        if ($ctx->{hoststab} and $ctx->{hoststab}->{$node} and $ctx->{hoststab}->{$node}->[0]->{ip}) {
            @ips = ($ctx->{hoststab}->{$node}->[0]->{ip});
        } else {
            @ips = getipaddr($node,GetAllAddresses=>1);
            if (not @ips and keys %{$ctx->{nodeips}->{$node}}) {
                @ips = keys %{$ctx->{nodeips}->{$node}};
            }
            if (!defined($ips[0])) {
                xCAT::SvrUtils::sendmsg([1,"Unable to find an IP for $node in hosts table or via system lookup (i.e. /etc/hosts)"], $callback);
                next;
            }
        }
        foreach my $ip (@ips) {
            $ctx->{currip}=$ip;
            #time to update, A and PTR records, IPv6 still TODO
            if ($ip =~ /\./) { #v4
                $ip = join('.',reverse(split(/\./,$ip)));
                $ip .= '.IN-ADDR.ARPA.';
            } elsif ($ip =~ /:/) { #v6
                $ip=getipaddr($ip,GetNumber=>1);
                $ip=$ip->as_hex();
                $ip =~ s/^0x//;
                $ip = join('.',reverse(split(//,$ip)));
                $ip .= '.ip6.arpa.';
            } else {
                die "ddns did not understand $ip result of lookup";
            }
            #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;
            my $tmpdm;
            unless ($domain =~ /\.$/) { $tmpdm = $domain.'.'; } #example.com becomes example.com.

            find_nameserver_for_dns($ctx,$revzone);
            find_nameserver_for_dns($ctx,$tmpdm);
        }
    }
    my $zone;
    foreach $zone (keys %{$ctx->{updatesbyzone}}) {
        my $ip = xCAT::NetworkUtils->getipaddr($ctx->{nsmap}->{$zone});
        if( !defined $ip) {
            xCAT::SvrUtils::sendmsg([1,"Please make sure $ctx->{nsmap}->{$zone} exist either in /etc/hosts or DNS."], $callback);
            return 1;
        }

        my $resolver = Net::DNS::Resolver->new(nameservers=>[$ip]);
        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}}) {
            if ($ctx->{deletemode}) {
                $update->push(update=>rr_del($entry));
            } else {
                $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)
                {
                    if ($reply->header->rcode ne 'NOERROR')
                    {
                        xCAT::SvrUtils::sendmsg([1,"Failure encountered updating $zone, error was ".$reply->header->rcode.". See more details in system log."], $callback);
                    }
                }
                else
                {
                    xCAT::SvrUtils::sendmsg([1,"No reply received when sending DNS update to zone $zone"], $callback);
                }
                
                $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);
                if ($reply)
                {
                    if ($reply->header->rcode ne 'NOERROR')
                    {
                        xCAT::SvrUtils::sendmsg([1,"Failure encountered updating $zone, error was ".$reply->header->rcode.". See more details in system log."], $callback);
                    }
                }
                else
                {
                    xCAT::SvrUtils::sendmsg([1,"No reply received when sending DNS update to zone $zone"], $callback);
                }
            
            # sometimes resolver does not work if the update zone request sent so quick
            sleep 1;
        }
    }
    xCAT::SvrUtils::sendmsg("Completed updating DNS records.", $callback);
}
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;
    if ($ip =~ /:/) {
        @rrcontent = ( "$name IN AAAA $ip" );
    } else {
        @rrcontent = ( "$name IN A $ip" );
    }
    foreach (keys %{$ctx->{nodeips}->{$node}}) {
        unless ($_ eq $ip) {
            if ($_ =~ /:/) {
                push @rrcontent,"$name IN AAAA $_";
            } else {
                push @rrcontent,"$name IN A $_";
            }
        }
    }

    if (defined $ctx->{aliases}->{$node}) {
    foreach (keys %{$ctx->{aliases}->{$node}}) {
        push @rrcontent, "$_ IN CNAME $name";
    }
    }
    if ($ctx->{deletemode}) {
        push @rrcontent,"$name TXT";
        push @rrcontent,"$name A";
    }
    if ($zone =~ /IN-ADDR.ARPA/ or $zone =~ /ip6.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
           } elsif ($::XCATSITEVALS{ddnsserver}) {
                $ctx->{nsmap}->{$zone} = $::XCATSITEVALS{ddnsserver};
           } else {
               my $reply = $ctx->{resolver}->query($zone,'NS');
               if ($reply)  {
                   if ($reply->header->rcode ne 'NOERROR') {
                      xCAT::SvrUtils::sendmsg([1,"Failure encountered querying $zone, error was ".$reply->header->rcode], $callback);
                    }
                    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 !~ /\./) {
               xCAT::SvrUtils::sendmsg([1,"Unable to find reverse zone to hold $node"], $callback,$node);
               last;
            }

           $zone =~ s/^[^\.]*\.//; #strip all up to and including first dot
           unless ($zone) {
               xCAT::SvrUtils::sendmsg([1,"Unable to find zone to hold $node"], $callback,$node);
               last;
           }
       }
    }
}
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;
}

sub makedns_usage
{
    my $callback = shift;

    my $rsp;
    push @{$rsp->{data}},
      "\n  makedns - sets up domain name services (DNS).";
    push @{$rsp->{data}}, "  Usage: ";
    push @{$rsp->{data}}, "\tmakedns [-h|--help ]";
    push @{$rsp->{data}}, "\tmakedns [-V|--verbose]";
    push @{$rsp->{data}}, "\tmakedns [-V|--verbose] [-e|--external] [-n|--new ] [noderange]";
    push @{$rsp->{data}}, "\tmakedns [-V|--verbose] [-e|--external] [-d|--delete noderange]";
    push @{$rsp->{data}}, "\n";
    xCAT::MsgUtils->message("I", $rsp, $callback);
    return 0;
}

sub get_dns_slave
{
    # get all service nodes with servicenode.nameserver=2
    my @sns;
    my @slaves;
    my $sntab = xCAT::Table->new('servicenode');
    my @ents = $sntab->getAllAttribs('node', 'nameserver');

    foreach my $sn (@ents)
    {
        if ($sn->{'nameserver'} == 2)
        {
            push @sns, $sn->{'node'};
        }
    }

    @slaves = xCAT::NodeRange::noderange(join(',',@sns));
    return \@slaves;
}

1;
