diff --git a/ChangeLog.md b/ChangeLog.md index 579f686..a856fd9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -16,6 +16,11 @@ repository history](https://github.com/ddclient/ddclient/commits/main). special characters are preserved literally. [#766](https://github.com/ddclient/ddclient/pull/766) +### New features + + * `dnsexit2`: Multiple hosts are updated in a single API call when possible. + [#684](https://github.com/ddclient/ddclient/pull/684) + ## 2025-01-07 v4.0.0-rc.2 ### Breaking changes diff --git a/ddclient.in b/ddclient.in index fea83bb..d296b5e 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4097,54 +4097,55 @@ EoEXAMPLE ###################################################################### sub nic_dnsexit2_update { my $self = shift; - # The DNSExit API does not support updating hosts with different zones at the same time, - # handling update per host. for my $h (@_) { $config{$h}{'zone'} = $h if !defined(opt('zone', $h)); - dnsexit2_update_host($h); } + dnsexit2_update_hostgroup($_) for group_hosts_by(\@_, qw(password path server ssl zone)); } -sub dnsexit2_update_host { - my ($h) = @_; - local $_l = pushlogctx($h); - my $name = $h; - # Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or - # set to the empty string; both have identical semantics. For consistency, always - # remove the zone even if it means $name becomes the empty string. - my $zone = opt('zone', $h); - if ($name =~ s/(?:^|\.)\Q$zone\E$//) { - # The zone was successfully trimmed from $name. - } else { - fatal("hostname does not end with the zone: " . opt('zone', $h)); - } - # The IPv4 and IPv6 addresses must be updated together in a single API call. - my %ips; +sub dnsexit2_update_hostgroup { + my ($group) = @_; + return unless @{$group->{hosts}} > 0; + local $_l = pushlogctx(join(', ', @{$group->{hosts}})); + my %hostips; my @updates; - for my $ipv ('4', '6') { - my $ip = delete($config{$h}{"wantipv$ipv"}) or next; - $ips{$ipv} = $ip; - info("updating IPv$ipv address to $ip"); - $recap{$h}{"status-ipv$ipv"} = 'failed'; - push(@updates, { - name => $name, - type => ($ipv eq '6') ? 'AAAA' : 'A', - content => $ip, - ttl => opt('ttl', $h), - }); - }; - my $url = opt('server', $h) . opt('path', $h); + for my $h (@{$group->{hosts}}) { + local $_l = pushlogctx($h) if @{$group->{hosts}} > 1; + my $name = $h; + # Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or + # set to the empty string; both have identical semantics. For consistency, always + # remove the zone even if it means $name becomes the empty string. + if ($name =~ s/(?:^|\.)\Q$group->{cfg}{'zone'}\E$//) { + # The zone was successfully trimmed from $name. + } else { + fatal("hostname does not end with the zone: $group->{cfg}{'zone'}"); + } + # The IPv4 and IPv6 addresses must be updated together in a single API call. + for my $ipv ('4', '6') { + my $ip = delete($config{$h}{"wantipv$ipv"}) or next; + $hostips{$h}{$ipv} = $ip; + info("updating IPv$ipv address to $ip"); + $recap{$h}{"status-ipv$ipv"} = 'failed'; + push(@updates, { + name => $name, + type => ($ipv eq '6') ? 'AAAA' : 'A', + content => $ip, + ttl => opt('ttl', $h), + }); + } + } + return unless @updates > 0; my $reply = geturl( proxy => opt('proxy'), - url => $url, + url => $group->{cfg}{'server'} . $group->{cfg}{'path'}, headers => [ 'Content-Type: application/json', 'Accept: application/json', ], method => 'POST', data => encode_json({ - apikey => opt('password', $h), - domain => $zone, + apikey => $group->{cfg}{'password'}, + domain => $group->{cfg}{'zone'}, update => \@updates, }), ); @@ -4191,12 +4192,15 @@ sub dnsexit2_update_host { return; } success($message); - $recap{$h}{'mtime'} = $now; - keys(%ips); # Reset internal iterator. - while (my ($ipv, $ip) = each(%ips)) { - $recap{$h}{"ipv$ipv"} = $ip; - $recap{$h}{"status-ipv$ipv"} = 'good'; - success("updated IPv$ipv address to $ip"); + keys(%hostips); # Reset internal iterator. + while (my ($h, $ips) = each(%hostips)) { + $recap{$h}{'mtime'} = $now; + keys(%$ips); # Reset internal iterator. + while (my ($ipv, $ip) = each(%$ips)) { + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{"status-ipv$ipv"} = 'good'; + success("updated IPv$ipv address to $ip"); + } } } diff --git a/t/protocol_dnsexit2.pl b/t/protocol_dnsexit2.pl index 57aa24e..9991e7c 100644 --- a/t/protocol_dnsexit2.pl +++ b/t/protocol_dnsexit2.pl @@ -165,12 +165,6 @@ my @test_cases = ( ttl => 5, type => 'A', }, - ], - }, - { - apikey => 'key', - domain => 'example.com', - update => [ { content => '2001:db8::1', name => 'host2',