# HG changeset patch # User Heiko Schlittermann (JUMPER) # Date 1400965277 -7200 # Node ID aa1598910bb0bf0d3118fea3e51451b86d5ce95c # Parent 0f37544f1b984635fc420a01c5a4534eea30c659 moved more functions to the lib and put there some code to save the new zone in case something happens. diff -r 0f37544f1b98 -r aa1598910bb0 bin/dnsvi --- a/bin/dnsvi Fri May 23 16:15:41 2014 +0200 +++ b/bin/dnsvi Sat May 24 23:01:17 2014 +0200 @@ -1,39 +1,46 @@ #! /usr/bin/perl -#line 2 +#line 3 use 5.010; use strict; use warnings; use if $ENV{DEBUG} // '' eq 'dnsvi' => 'Smart::Comments'; -use File::Temp; use Getopt::Long; use Pod::Usage; use blib; use DNS::Vi; +sub slurp { + local $/ = undef; + local @ARGV = @_; + <>; +} + sub main { my %o = ( - local => undef, + local => undef, key => undef, server => undef, debug => undef, - editor => $ENV{EDITOR}//'vi', + editor => $ENV{EDITOR} // 'vi', + skip => [qw/NS RRSIG NSEC3 NSEC3PARAM NSEC DNSKEY TSIG/], ); GetOptions( 'k|key=s' => \$o{key}, 's|server=s' => \$o{server}, 'd|debug!' => \$o{debug}, - 'l|local!' => \$o{local}, - 'editor=s' => \$o{editor}, + 'l|local!' => \$o{local}, + 'editor=s' => \$o{editor}, ) - && @ARGV == 1 + && @ARGV >= 1 or pod2usage(); my $zone = shift @ARGV; - $o{server} = $o{local} ? 'localhost' : (split ' ', `dig +short soa $zone`)[0] - if not defined $o{server}; + $o{server} = + $o{local} ? 'localhost' : (split ' ', `dig +short soa $zone`)[0] + if not defined $o{server}; my @dig = ( dig => 'AXFR', @@ -42,59 +49,38 @@ $zone ); - my @zone1 = grep { - not $_->{rrset}{rrtype} ~~ - [qw(RRSIG NSEC3 NSEC3PARAM NSEC DNSKEY TSIG)] - } parse($_ = `@dig`) or die $_; + my @zone1 = parse($_ = `@dig`, { -skip => $o{skip} } ) + or die "Empty zone\n"; + my @zone2 = do { + if (my $file = shift @ARGV) { + parse(slurp($file), { -skip => $o{skip} }); + } + else { + edit(@zone1, { -skip => $o{skip}, -editor => $o{editor} }); + } + }; + ### @zone2 - my $tmp = File::Temp->new(); - $tmp->print(nice @zone1); - $tmp->flush(); - system $o{editor} => $tmp->filename; - $tmp->seek(0, 0); - my @zone2 = parse(<$tmp>); my ($add, $del) = delta(\@zone1, \@zone2); - if ((@$add + @$del) == 0) { - say 'Nothing changed'; - return 0; + say 'nothing changed'; + return 0; } - my $orig_soa = - (grep { $_->{rrtype} eq 'SOA' } map { $_->{rrset} } @zone1)[0]; - - my @cmds = ( - $o{local} ? () : "server $o{server}", - "prereq yxrrset @{$orig_soa}{qw{label rrtype data}}", - (map { "update delete $_" } @$del), - (map { "update add $_" } @$add), - 'show', - 'send', - 'answer', - ); - - print <<_EOF, join "\n" => @cmds, '', ''; -# The following commands are about to be sent via nsupdate -# to the master server: - -_EOF - print '# Please confirm (yes/NO): '; + say 'The following changes need your confirmation.'; + say join "\n", show($add, $del); + print 'confirm (yes|NO): '; return 1 if !~ /^y/i; - my @nsupdate = ( - 'nsupdate', - defined $o{debug} ? ('-d') : (), - defined $o{key} ? (-k => $o{key}) : (), - defined $o{local} ? ('-l') : (), - ); - - open(my $nsupdate, '|-') or do { - exec @nsupdate; - die "Can't exec @nsupdate: $!\n"; + update(\@zone1, $add, $del, { + -server => $o{server}, + -local => $o{local}, + -debug => $o{debug}, + -key => $o{key}}) + or do { + save(\@zone2, ",dnsvi-$$") + and say "Saved as ',dnsvi-$$'"; }; - say $nsupdate join "\n", @cmds; - close($nsupdate); - say "nsupdate returned $?"; return 0; } @@ -109,7 +95,7 @@ =head1 SYNOPSIS - vidns [-l] [-k key] [-s server] [-d] + vidns [-l] [-k key] [-s server] [-d] [] =head1 DESCRIPTION diff -r 0f37544f1b98 -r aa1598910bb0 lib/DNS/Vi.pm --- a/lib/DNS/Vi.pm Fri May 23 16:15:41 2014 +0200 +++ b/lib/DNS/Vi.pm Sat May 24 23:01:17 2014 +0200 @@ -4,14 +4,16 @@ use warnings; use if $ENV{DEBUG}//'' eq 'dnsvi' => 'Smart::Comments'; use Digest::SHA qw(sha512_hex); +use File::Temp; use base 'Exporter'; -our @EXPORT = qw(ttl2h h2ttl parse delta nice); +our @EXPORT = qw(ttl2h h2ttl parse delta nice edit update show save); our @EXPORT_OK = (); sub parse { - my $data = join '', @_; + my %arg = %{pop @_} if ref $_[-1] eq 'HASH'; + my $data = shift; my @lines = split /\n/, $data; my @zone; @@ -42,6 +44,7 @@ rrtype => uc $+{rrtype}, data => $+{data}, ); + next if $rrset{rrtype} ~~ $arg{-skip}; if ($rrset{rrtype} eq 'SOA') { next if $soa_seen; @@ -117,8 +120,9 @@ return $out // $ttl; } +{ + my %order = map { state $n = 0; $_ => ++$n } qw(SOA NS TXT MX A AAAA); sub nice { - my %order = map { state $n = 0; $_ => ++$n } qw(SOA NS TXT MX A AAAA); # get a list of { id => $id, rrset => \%rrset } my @zone = @@ -155,9 +159,9 @@ $r{data}; }; push @out, $print->($_) foreach @zone; - return join "\n", @out; + return join "\n", @out, ''; } - +} sub delta { my ($zone1, $zone2) = @_; my %zone1 = map { $_->{id}, $_->{rrset} } @$zone1; @@ -174,4 +178,66 @@ return (\@add, \@del); } +sub edit { + my %arg = %{pop @_} if ref $_[-1] eq 'HASH'; + my @zone = @_; + + my $tmp = File::Temp->new(); + $tmp->print(nice @zone); + $tmp->flush(); + system $arg{-editor} => $tmp->filename; + $tmp->seek(0, 0); + return parse(do { local $/ = undef; <$tmp>}, {-skip => $arg{-skip}}); +} + +sub show { + my ($add, $del) = @_; + my @out = ( + (map { " - $_ " } @$del), + (map { " + $_ " } @$add), + ); + return @out; +} + +sub update { + my %arg = %{pop @_} if ref $_[-1] eq 'HASH'; + my ($zone1, $add, $del) = @_; + + my $orig_soa = + (grep { $_->{rrtype} eq 'SOA' } map { $_->{rrset} } @$zone1)[0]; + + my @cmds = ( + $arg{-local} ? () : "server $arg{-server}", + "prereq yxrrset @{$orig_soa}{qw{label rrtype data}}", + (map { "update delete $_" } @$del), + (map { "update add $_" } @$add), + 'show', + 'send', + 'answer', + ); + my @nsupdate = ( + 'nsupdate', + defined $arg{-debug} ? ('-d') : (), + defined $arg{-key} ? (-k => $arg{-key}) : (), + defined $arg{-local} ? ('-l') : (), + ); + + open(my $nsupdate, '|-') or do { + exec @nsupdate; + die "Can't exec @nsupdate: $!\n"; + }; + say $nsupdate join "\n", @cmds; + close($nsupdate); + say "nsupdate returned $?"; + return $? ? undef : 1; +} + +sub save { + my ($zone, $file) = @_; + open(my $fh, '>', $file) or die "Can't open >$file: $!\n"; + print $fh nice @$zone; + close($fh); + +} + 1; diff -r 0f37544f1b98 -r aa1598910bb0 t/10-vidns.t --- a/t/10-vidns.t Fri May 23 16:15:41 2014 +0200 +++ b/t/10-vidns.t Sat May 24 23:01:17 2014 +0200 @@ -5,37 +5,43 @@ use warnings; use_ok 'DNS::Vi' or BAIL_OUT 'DNS::Vi not found!'; -can_ok 'DNS::Vi', qw(ttl2h h2ttl parse nice delta); +can_ok 'DNS::Vi', qw(ttl2h h2ttl parse nice delta edit update show); # TODO: more tests! is ttl2h(86400), '1d', '-> 1d'; is h2ttl('1d'), 86400, '<- 1d'; -my $data = do { +my $data1 = do { local $/ = undef; local @ARGV = 't/samples/kugelbus-axfr'; <>; }; -my @zone1 = parse($data); -is @zone1, 64 => '64 rrsets'; +# check parser with and without skip list +is parse($data1), 64 => '64 rrsets'; +my @zone1 = + parse($data1, { -skip => [qw(RRSIG NSEC3 NSEC3PARAM NSEC DNSKEY TSIG)] }); +is @zone1, 18 => '18 rrsets'; -$data =~ s{^kugelbus.*?IN\s+MX.*?$}{}m; -$data .= <<_; -foo 1h txt "foo" - 30m A 1.1.1.1 -_ -my @zone2 = parse($data); -is @zone2, 65, => '65 rrsets'; +subtest 'parsed data' => sub { + my ($soa) = + map { $_->{rrset} } grep { $_->{rrset}{rrtype} eq 'SOA' } @zone1; + is ref $soa, 'HASH' => 'got result hash'; + is $soa->{rrtype}, 'SOA' => 'is SOA'; + is $soa->{data}, + 'pu.schlittermann.de. ' + . 'hostmaster.net.schlittermann.de. ' + . '18 86400 7200 604800 86400' => 'has expected data'; -my ($add, $del) = delta(\@zone1, \@zone2); -is @$add, 2 => '2 sets to add'; -is @$del, 1 => '1 set to delete'; + my @txt = map { $_->{rrset} } grep { $_->{rrset}{rrtype} eq 'TXT' } @zone1; + is @txt, 3 => 'got 3 txt records'; +}; -is_deeply $add, - ['foo.kugelbus.de. 1800 A 1.1.1.1', 'foo.kugelbus.de. 3600 TXT "foo"'] => - 'added datasets'; -is_deeply $del, - ['kugelbus.de. 86400 MX 10 ssl.schlittermann.de.'] => 'removed datasets'; +# delta should find noting +subtest 'delta' => sub { + my ($add, $del) = delta(\@zone1, \@zone1); + is @$add, 0 => 'nothing added'; + is @$del, 0 => 'nothing deleted'; +}; done_testing();