diff --git a/ChangeLog.md b/ChangeLog.md index 69c2f76..3a0215a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -100,6 +100,8 @@ repository history](https://github.com/ddclient/ddclient/commits/master). ### Bug fixes + * Fixed numerous bugs in cache file (recap) handling. + [#740](https://github.com/ddclient/ddclient/pull/740) * Fixed numerous bugs in command-line option and configuration file processing. [#733](https://github.com/ddclient/ddclient/pull/733) * `noip`: Fixed failure to honor IP discovery settings in some circumstances. diff --git a/ddclient.in b/ddclient.in index 957b05b..a2c250b 100755 --- a/ddclient.in +++ b/ddclient.in @@ -145,15 +145,10 @@ our %config; # host's "recap variables" -- the host's protocol's variables with a truthy `recap` property -- are # included. # -# `%config` is the source of truth for recap variable values. The recap variable values in -# `%config` are initialized with values from the cache file (via `%recap`). After initialization, -# any change to a recap variable's value is made to `%config` and later copied to `%recap` before -# being written to the cache file. Changes made directly to `%recap` are erroneous because they -# will be overwritten by the value in `%config`. -# -# There are two classes of recap variables: +# `%recap` is independent of `%config`, although they both share the same set of variable +# declarations. There are two classes of recap variables: # * "Status" variables: These track update success/failure, the IP address of the last successful -# update, etc. These do not hold configuration data. These should always be kept in sync with +# update, etc. These do not hold configuration data; they are unrelated to any entries in # `%config`. # * "Configuration change detection" variables: These are used to force an update if the value in # the same-named entry in `%config` has changed since the previous update attempt. The value @@ -654,20 +649,13 @@ our %variables = ( 'min-error-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0), # The desired IP address (IPv4 or IPv6, but almost always IPv4) that should be saved at the - # DDNS service. - # TODO: Legacy protocol implementations write the IP address most recently saved at the - # DDNS service to this variable so that it can be saved in the recap (as `ipv4` or `ipv6`). - # Update the legacy implementations to use `ipv4` or `ipv6` instead, though see the TODO - # for those variables. + # DDNS service (when `use=ip`). 'ip' => setv(T_IP, 0, 0, undef, undef), # As a recap value, this is the IPv4 address most recently saved at the DDNS service. As a # setting, this is the desired IPv4 address that should be saved at the DDNS service. - # TODO: The use of `ipv4` as a recap status variable is supposed to be independent of the - # use of `ipv4` as a configuration setting. Unfortunately, because the `%config` value is - # synced to `%recap`, the two uses conflict which causes the bug "skipped: IP address was - # already set to a.b.c.d" when the IP was never set to a.b.c.d. Rename the `%recap` status - # variable to something like `saved-ipv4` to avoid the collision (and confusion) with the - # `%config` setting variable. + # TODO: The use of `ipv4` as a recap status variable is independent of the use of `ipv4` as + # a configuration setting. Rename the `%recap` status variable to something like + # `saved-ipv4` to avoid confusion with the `%config` setting variable. 'ipv4' => setv(T_IPV4, 0, 1, undef, undef), # As `ipv4`, but for an IPv6 address. 'ipv6' => setv(T_IPV6, 0, 1, undef, undef), @@ -711,7 +699,7 @@ our %variables = ( ); # Converts a legacy protocol update method to behave like a modern implementation by moving -# `$config{$h}{status}` and `$config{$h}{ip}` to the version-specific modern equivalents. +# `$recap{$h}{status}` and `$recap{$h}{ip}` to the version-specific modern equivalents. sub adapt_legacy_update { my ($update) = @_; return sub { @@ -720,7 +708,7 @@ sub adapt_legacy_update { for my $h (@hosts) { $ipv{$h} = defined($config{$h}{'wantipv4'}) ? '4' : '6'; $config{$h}{'wantip'} = delete($config{$h}{"wantipv$ipv{$h}"}); - delete($config{$h}{$_}) for qw(ip status); + delete($recap{$h}{$_}) for qw(ip status); } $update->(@hosts); for my $h (@hosts) { @@ -728,29 +716,8 @@ sub adapt_legacy_update { delete($config{$h}{'wantip'}); debug( "legacy protocol; moving 'status' to 'status-ipv$ipv{$h}', 'ip' to 'ipv$ipv{$h}'"); - $config{$h}{"status-ipv$ipv{$h}"} = delete($config{$h}{'status'}); - # TODO: Currently $config{$h}{'ip'} is used for two distinct purposes: it holds the - # value of the --ip option, and it is updated by legacy protocols to hold the new IP - # address after an update. Fortunately, the --ip option is not used very often, and if - # it is, the values for the two use cases are usually (but not always) the same. This - # boolean is an imperfect attempt to identify whether 'ip' is being used for the --ip - # option, to avoid breaking some user's configuration. Protocols should be updated to - # not set $config{$h}{'ip'} because %config is for configuration, not update status - # (same goes for 'status', 'mtime', etc.). - my $ip_option = opt('use', $h) eq 'ip' || opt('usev6', $h) eq 'ip'; - my $ip = $config{$h}{'ip'}; - delete($config{$h}{'ip'}) if !$ip_option; - # TODO: See above comment for $ip_option. This is the same situation, but for 'ipv4' - # and 'ipv6'. - my $vip_option = opt("usev$ipv{$h}", $h) eq "ipv$ipv{$h}"; - if ($vip_option && defined(my $vip = $config{$h}{"ipv$ipv{$h}"})) { - if (!defined($ip) || $ip ne $vip) { - my $fip = defined($ip) ? "'$ip'" : ''; - debug("unable to set 'ipv$ipv{$h}' to $fip; already set to '$vip'"); - } - next; - } - $config{$h}{"ipv$ipv{$h}"} = $ip; + $recap{$h}{"status-ipv$ipv{$h}"} = delete($recap{$h}{'status'}); + $recap{$h}{"ipv$ipv{$h}"} = delete($recap{$h}{'ip'}); } }; } @@ -1502,29 +1469,39 @@ sub update_nics { $0 = sprintf("%s - updating %s", $program, join(',', @hosts)); local $_l = pushlogctx($p); for my $h (@hosts) { - $config{$h}{'atime'} = $now; - delete($config{$h}{$_}) for qw(status-ipv4 status-ipv6 wtime - warned-min-interval warned-min-error-interval); + $recap{$h}{'atime'} = $now; + delete($recap{$h}{$_}) for qw(status-ipv4 status-ipv6 wtime + warned-min-interval warned-min-error-interval); + # Update configuration change detection variables in `%recap` with the latest + # values in `%config`. Notes about why this is done here: + # * The vars must not be updated if the host is not being updated because change + # detection is defined relative to the previous update attempt. In particular, + # these can't be updated when the protocol's `force_update` method is called + # because that method is not always called before an update is attempted. + # * The vars must be updated after the `force_update` method would be called so + # that `force_update` can check whether any settings have changed since the + # last time an update was attempted. + # * The vars are updated before the protocol's `update` method is called so that + # `update` sees consistent values between `%recap` and `%config`. This reduces + # the impact of Hyrum's Law; if a protocol needs a variable to be updated after + # the `update` method is called then that behavior should be made explicit. + my $vars = $protocols{opt('protocol', $h)}{variables}; + for my $v (qw(static wildcard mx backupmx)) { + next if !$vars->{$v} || !$vars->{$v}{recap}; + if (defined(my $val = opt($v, $h))) { + $recap{$h}{$v} = $val; + } else { + # Entries in `%recap` with `undef` values are deleted to avoid needing to + # figure out how to represent `undef` in the cache file and to simplify + # testing. + delete($recap{$h}{$v}); + } + } } &$update(@hosts); for my $h (@hosts) { delete($config{$h}{$_}) for qw(wantipv4 wantipv6); } - for my $h (@hosts) { - # Update `%recap` with the latest values in `%config`. This is done after the - # protocol's update method returns in case that method mutates any of the `%config` - # values or needs access to the previous iteration's `%recap` values. Entries in - # `%recap` with `undef` values are deleted to avoid needing to figure out how to - # represent `undef` in the cache file and to simplify testing. Also, entries for - # non-recap variables (which can happen after changing a host's protocol or after - # switching to a different version of ddclient) are deleted. - my $vars = $protocols{$p}{variables}; - $recap{$h} = {}; - for my $v (keys(%$vars)) { - next if !$vars->{$v}{recap} || !defined(opt($v, $h)); - $recap{$h}{$v} = opt($v, $h); - } - } runpostscript(join ' ', keys %ipsv4, keys %ipsv6); } } @@ -1614,7 +1591,6 @@ sub read_recap { %opt = (); $saved_recap = _read_config(\%recap, $globals, "##\\s*$program-$version\\s*", $file); %opt = %saved; - $recap{$_} //= {} for keys(%config); for my $h (keys(%recap)) { if (!exists($config{$h})) { delete($recap{$h}); @@ -1624,26 +1600,6 @@ sub read_recap { for my $v (keys(%{$recap{$h}})) { delete($recap{$h}{$v}) if !$vars->{$v} || !$vars->{$v}{recap}; } - # Only status variables are copied from `%recap` into `%config` because the recap should - # not change the configuration. See the documenting comments for `%recap`. - # TODO: Hard-coding this list impairs readability and maintainability. In particular, - # different pieces of code need to know the list of status variables, and keeping them all - # in sync is error-prone (DRY). Also, different protocols might need different status - # variables. - # TODO: None of the recap variables should be copied into `%config` because `%config` is - # not the semantically appropriate place to record update status. Code that currently - # reads/sets `$config{$h}{'status-ipv4'}` (and friends) should instead access - # `$recap{$h}{'status-ipv4'}` directly. - for my $v (qw(atime mtime wtime ipv4 ipv6 status-ipv4 status-ipv6 - warned-min-interval warned-min-error-interval)) { - next if !$vars->{$v} || !$vars->{$v}{recap}; - if (defined($recap{$h}{$v})) { - $config{$h}{$v} = $recap{$h}{$v}; - } else { - delete($config{$h}{$v}); - } - } - delete($recap{$h}) if !%{$recap{$h}}; } } ###################################################################### @@ -3519,15 +3475,14 @@ sub nic_updateable { if (($recap{$host}{'status-ipv4'} // '') eq 'good' && !interval_expired($host, 'mtime', 'min-interval')) { warning("skipped update from $previpv4 to $ipv4 because it has been less than $prettyi{'min-interval'} since the previous update (on $prettyt{'mtime'})") - if opt('verbose') || !($config{$host}{'warned-min-interval'} // 0); + if opt('verbose') || !($recap{$host}{'warned-min-interval'} // 0); - $config{$host}{'warned-min-interval'} = $now; $recap{$host}{'warned-min-interval'} = $now; } elsif (($recap{$host}{'status-ipv4'} // '') ne 'good' && !interval_expired($host, 'atime', 'min-error-interval')) { - if (opt('verbose') || (!$config{$host}{'warned-min-error-interval'} && + if (opt('verbose') || (!$recap{$host}{'warned-min-error-interval'} && ($warned_ipv4{$host} // 0) < $inv_ip_warn_count)) { warning("skipped update from $previpv4 to $ipv4 because it has been less than $prettyi{'min-error-interval'} since the previous update attempt (on $prettyt{'atime'}), which failed"); if (!$ipv4 && !opt('verbose')) { @@ -3537,7 +3492,6 @@ sub nic_updateable { } } - $config{$host}{'warned-min-error-interval'} = $now; $recap{$host}{'warned-min-error-interval'} = $now; } else { @@ -3548,15 +3502,14 @@ sub nic_updateable { if (($recap{$host}{'status-ipv6'} // '') eq 'good' && !interval_expired($host, 'mtime', 'min-interval')) { warning("skipped update from $previpv6 to $ipv6 because it has been less than $prettyi{'min-interval'} since the previous update (on $prettyt{'mtime'})") - if opt('verbose') || !($config{$host}{'warned-min-interval'} // 0); + if opt('verbose') || !($recap{$host}{'warned-min-interval'} // 0); - $config{$host}{'warned-min-interval'} = $now; $recap{$host}{'warned-min-interval'} = $now; } elsif (($recap{$host}{'status-ipv6'} // '') ne 'good' && !interval_expired($host, 'atime', 'min-error-interval')) { - if (opt('verbose') || (!$config{$host}{'warned-min-error-interval'} && + if (opt('verbose') || (!$recap{$host}{'warned-min-error-interval'} && ($warned_ipv6{$host} // 0) < $inv_ip_warn_count)) { warning("skipped update from $previpv6 to $ipv6 because it has been less than $prettyi{'min-error-interval'} since the previous update attempt (on $prettyt{'atime'}, which failed"); if (!$ipv6 && !opt('verbose')) { @@ -3566,7 +3519,6 @@ sub nic_updateable { } } - $config{$host}{'warned-min-error-interval'} = $now; $recap{$host}{'warned-min-error-interval'} = $now; } else { @@ -3712,16 +3664,16 @@ sub nic_dyndns1_update { } if ($return_code ne 'NOERROR' || $error_code ne 'NOERROR' || !$title) { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; $title = 'incomplete response from ' . opt('server', $h) unless $title; warning("SENT: %s", $url) unless opt('verbose'); warning("REPLIED: %s", $reply); failed($title); next; } - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("$return_code: IP address set to $ip ($title)"); } } @@ -3868,8 +3820,8 @@ sub nic_dyndns2_update { warning("$status: $errors{$status}"); $status = 'good'; } - $config{$h}{'status-ipv4'} = $status if $ipv4; - $config{$h}{'status-ipv6'} = $status if $ipv6; + $recap{$h}{'status-ipv4'} = $status if $ipv4; + $recap{$h}{'status-ipv6'} = $status if $ipv6; if ($status ne 'good') { if (exists($errors{$status})) { failed("$status: $errors{$status}"); @@ -3887,9 +3839,9 @@ sub nic_dyndns2_update { # some services do not return the IP; and (2) comparison is brittle (e.g., # 192.000.002.001 vs. 192.0.2.1) and false errors could cause high load on the service # (an update attempt every min-error-interval instead of every max-interval). - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; + $recap{$h}{'ipv4'} = $ipv4 if $ipv4; + $recap{$h}{'ipv6'} = $ipv6 if $ipv6; + $recap{$h}{'mtime'} = $now; success("IPv4 address set to $ipv4") if $ipv4; success("IPv6 address set to $ipv6") if $ipv6; } @@ -3970,7 +3922,7 @@ sub dnsexit2_update_host { my $ip = delete($config{$h}{"wantipv$ipv"}) or next; $ips{$ipv} = $ip; info("updating IPv$ipv address to $ip"); - $config{$h}{"status-ipv$ipv"} = 'failed'; + $recap{$h}{"status-ipv$ipv"} = 'failed'; push(@updates, { name => $name, type => ($ipv eq '6') ? 'AAAA' : 'A', @@ -4036,11 +3988,11 @@ sub dnsexit2_update_host { return; } success($message); - $config{$h}{'mtime'} = $now; + $recap{$h}{'mtime'} = $now; keys(%ips); # Reset internal iterator. while (my ($ipv, $ip) = each(%ips)) { - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{"status-ipv$ipv"} = 'good'; success("updated IPv$ipv address to $ip"); } } @@ -4100,27 +4052,27 @@ sub nic_noip_update { for my $ip (split_by_comma($returnedips)) { next if (!$ip); my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; - $config{$h}{"status-ipv$ipv"} = $status; + $recap{$h}{"status-ipv$ipv"} = $status; } if ($status eq 'good') { - $config{$h}{'mtime'} = $now; + $recap{$h}{'mtime'} = $now; for my $ip (split_by_comma($returnedips)) { next if (!$ip); my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; - $config{$h}{"ipv$ipv"} = $ip; + $recap{$h}{"ipv$ipv"} = $ip; success("$status: IPv$ipv address set to $ip"); } } elsif (exists $errors{$status}) { if ($status eq 'nochg') { warning("$status: $errors{$status}"); - $config{$h}{'mtime'} = $now; + $recap{$h}{'mtime'} = $now; for my $ip (split_by_comma($returnedips)) { next if (!$ip); my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{"status-ipv$ipv"} = 'good'; } } else { failed("$status: $errors{$status}"); @@ -4135,7 +4087,7 @@ sub nic_noip_update { ($scale, $units) = (60*60, 'hours') if $units eq 'h'; $sec = $wait * $scale; - $config{$h}{'wtime'} = $now + $sec; + $recap{$h}{'wtime'} = $now + $sec; warning("$status: wait $wait $units before further updates"); } else { @@ -4236,13 +4188,13 @@ sub nic_dslreports1_update { } if ($return_code !~ /NOERROR/) { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; failed($reply); next; } - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("$return_code: IP address set to $ip"); } } @@ -4291,9 +4243,9 @@ sub nic_domeneshop_update { password => opt('password', $h), ); next if !header_ok($reply); - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-ipv$ipv"} = 'good'; success("IPv$ipv address set to $ip"); } } @@ -4381,14 +4333,14 @@ sub nic_zoneedit1_update { $status_ip = $var{'IP'} if exists $var{'IP'}; if ($status eq 'SUCCESS' || ($status eq 'ERROR' && $var{'CODE'} eq '707')) { - $config{$h}{'ip'} = $status_ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $status_ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip ($status_code: $status_text)"); } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; failed("$status_code: $status_text"); } shift @hosts; @@ -4491,7 +4443,7 @@ sub nic_easydns_update { # values are considered to be failures and will result in frequent retries (every # min-error-interval, which defaults to 5m). $status = 'good' if ($status // '') =~ qr/^NOERROR|OK$/; - $config{$h}{"status-ipv$ipv"} = $status; + $recap{$h}{"status-ipv$ipv"} = $status; if ($status ne 'good') { if (exists $errors{$status}) { failed("$status: $errors{$status}"); @@ -4500,8 +4452,8 @@ sub nic_easydns_update { } next; } - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{'mtime'} = $now; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{'mtime'} = $now; success("IPv$ipv address set to $ip"); } } @@ -4568,12 +4520,12 @@ sub nic_namecheap_update { my @reply = split /\n/, $reply; if (grep /0/i, @reply) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; failed("invalid reply: $reply"); } } @@ -4739,7 +4691,7 @@ sub nic_nfsn_update { if ($h eq $zone) { $name = ''; } elsif ($h !~ /$zone$/) { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; failed("$h is outside zone $zone"); next; } else { @@ -4754,7 +4706,7 @@ sub nic_nfsn_update { my $list_body = encode_www_form_urlencoded({name => $name, type => 'A'}); my $list_resp = nic_nfsn_make_request($h, $list_path, 'POST', $list_body); if (!header_ok($list_resp)) { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; nic_nfsn_handle_error($list_resp, $h); next; } @@ -4762,7 +4714,7 @@ sub nic_nfsn_update { $list_resp =~ s/^.*?\n\n//s; # Strip header my $list = eval { decode_json($list_resp) }; if ($@) { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; failed("JSON decoding failure"); next; } @@ -4779,7 +4731,7 @@ sub nic_nfsn_update { my $rm_resp = nic_nfsn_make_request($h, $rm_path, 'POST', $rm_body); if (!header_ok($rm_resp)) { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; nic_nfsn_handle_error($rm_resp, $h); next; } @@ -4794,12 +4746,12 @@ sub nic_nfsn_update { my $add_resp = nic_nfsn_make_request($h, $add_path, 'POST', $add_body); if (header_ok($add_resp)) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; nic_nfsn_handle_error($add_resp, $h); } } @@ -4903,11 +4855,11 @@ sub nic_njalla_update { } } if ($status eq 'good') { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; + $recap{$h}{'ipv4'} = $ipv4 if $ipv4; + $recap{$h}{'ipv6'} = $ipv6 if $ipv6; } - $config{$h}{'status-ipv4'} = $status if $ipv4; - $config{$h}{'status-ipv6'} = $status if $ipv6; + $recap{$h}{'status-ipv4'} = $status if $ipv4; + $recap{$h}{'status-ipv6'} = $status if $ipv6; } } @@ -4968,12 +4920,12 @@ sub nic_sitelutions_update { my @reply = split /\n/, $reply; if (grep /success/i, @reply) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; warning("SENT: %s", $url) unless opt('verbose'); warning("REPLIED: %s", $reply); failed("invalid reply"); @@ -5075,8 +5027,8 @@ sub nic_freedns_update { my $ipv6 = delete $config{$h}{'wantipv6'}; if ($record_list_error ne '') { - $config{$h}{'status-ipv4'} = 'failed' if ($ipv4); - $config{$h}{'status-ipv6'} = 'failed' if ($ipv6); + $recap{$h}{'status-ipv4'} = 'failed' if ($ipv4); + $recap{$h}{'status-ipv6'} = 'failed' if ($ipv6); failed($record_list_error); next; } @@ -5094,12 +5046,12 @@ sub nic_freedns_update { } info("setting IP address to $ip"); - $config{$h}{"status-ipv$ipv"} = 'failed'; + $recap{$h}{"status-ipv$ipv"} = 'failed'; if ($ip eq $rec->[1]) { - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-ipv$ipv"} = 'good'; success("update not necessary, '$type' record already set to $ip") if (!$daemon || opt('verbose')); } else { @@ -5113,9 +5065,9 @@ sub nic_freedns_update { if (header_ok($reply)) { $reply =~ s/^.*?\n\n//s; # Strip the headers. if ($reply =~ /Updated.*$h.*to.*$ip/) { - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-ipv$ipv"} = 'good'; success("IPv$ipv address set to $ip"); } else { warning("SENT: %s", $url_tmpl) unless opt('verbose'); @@ -5191,8 +5143,8 @@ sub nic_1984_update { next; } - $config{$host}{'status'} = 'good'; - $config{$host}{'ip'} = $ip; + $recap{$host}{'status'} = 'good'; + $recap{$host}{'ip'} = $ip; if ($response->{msg} =~ /unaltered/) { success("skipped: IP was already set to $response->{ip}"); } else { @@ -5259,12 +5211,12 @@ sub nic_changeip_update { my @reply = split /\n/, $reply; if (grep /success/i, @reply) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; warning("SENT: %s", $url) unless opt('verbose'); warning("REPLIED: %s", $reply); failed("invalid reply"); @@ -5380,9 +5332,9 @@ sub nic_godaddy_update { failed($msg); next; } - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-ipv$ipv"} = 'good'; success("updated successfully to $ip (status: $code)"); } } @@ -5444,7 +5396,7 @@ sub nic_henet_update { my ($line) = split(/\n/, $body, 2); my ($status, $returnedip) = split(/ /, lc($line)); $status = 'good' if $status eq 'nochg'; - $config{$h}{"status-ipv$ipv"} = $status; + $recap{$h}{"status-ipv$ipv"} = $status; if ($status ne 'good') { if (exists($errors{$status})) { failed("$status: $errors{$status}"); @@ -5454,8 +5406,8 @@ sub nic_henet_update { next; } success("$status: IPv$ipv address set to $returnedip"); - $config{$h}{"ipv$ipv"} = $returnedip; - $config{$h}{'mtime'} = $now; + $recap{$h}{"ipv$ipv"} = $returnedip; + $recap{$h}{'mtime'} = $now; } } } @@ -5524,8 +5476,8 @@ sub nic_mythicdyn_update { ); my $ok = header_ok($reply); if ($ok) { - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$mythver"} = "good"; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-ipv$mythver"} = "good"; success("IPv$mythver updated successfully"); } @@ -5634,12 +5586,12 @@ EoINSTR4 my $status = pipecmd($command, $instructions); if ($status eq 1) { for (@hosts) { - $config{$_}{'mtime'} = $now; + $recap{$_}{'mtime'} = $now; for my $ip ($ipv4, $ipv6) { next if (!$ip); my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; - $config{$_}{"ipv$ipv"} = $ip; - $config{$_}{"status-ipv$ipv"} = 'good'; + $recap{$_}{"ipv$ipv"} = $ip; + $recap{$_}{"status-ipv$ipv"} = 'good'; } } success("IPv4 address set to $ipv4") if $ipv4; @@ -5751,7 +5703,7 @@ sub nic_cloudflare_update { my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; info("setting IPv$ipv address to $ip"); - $config{$domain}{"status-ipv$ipv"} = 'failed'; + $recap{$domain}{"status-ipv$ipv"} = 'failed'; # Get DNS 'A' or 'AAAA' record ID $url = "https://" . opt('server', $domain) . "/zones/$zone_id/dns_records?"; @@ -5790,9 +5742,9 @@ sub nic_cloudflare_update { $response = eval {decode_json(${^MATCH})}; if ($response && $response->{result}) { success("IPv$ipv address set to $ip"); - $config{$domain}{"ipv$ipv"} = $ip; - $config{$domain}{'mtime'} = $now; - $config{$domain}{"status-ipv$ipv"} = 'good'; + $recap{$domain}{"ipv$ipv"} = $ip; + $recap{$domain}{'mtime'} = $now; + $recap{$domain}{"status-ipv$ipv"} = 'good'; } else { failed("invalid json or result"); } @@ -5874,7 +5826,7 @@ sub nic_hetzner_update { my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; info("setting IPv$ipv address to $ip"); - $config{$domain}{"status-ipv$ipv"} = 'failed'; + $recap{$domain}{"status-ipv$ipv"} = 'failed'; # Get DNS 'A' or 'AAAA' record ID $url = "https://" . opt('server', $domain) . "/records?zone_id=$zone_id"; @@ -5919,9 +5871,9 @@ sub nic_hetzner_update { $response = eval {decode_json(${^MATCH})}; if ($response && $response->{record}) { success("IPv$ipv address set to $ip"); - $config{$domain}{"ipv$ipv"} = $ip; - $config{$domain}{'mtime'} = $now; - $config{$domain}{"status-ipv$ipv"} = 'good'; + $recap{$domain}{"ipv$ipv"} = $ip; + $recap{$domain}{'mtime'} = $now; + $recap{$domain}{"status-ipv$ipv"} = 'good'; } else { failed("invalid json or result"); } @@ -6067,8 +6019,8 @@ sub nic_inwx_update { $status = 'good'; } for my $h (@hosts) { - $config{$h}{'status-ipv4'} = $status if $ipv4; - $config{$h}{'status-ipv6'} = $status if $ipv6; + $recap{$h}{'status-ipv4'} = $status if $ipv4; + $recap{$h}{'status-ipv6'} = $status if $ipv6; } if ($status ne 'good') { if (exists($errors{$status})) { @@ -6079,9 +6031,9 @@ sub nic_inwx_update { next; } for my $h (@hosts) { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; + $recap{$h}{'ipv4'} = $ipv4 if $ipv4; + $recap{$h}{'ipv6'} = $ipv6 if $ipv6; + $recap{$h}{'mtime'} = $now; } success("IPv4 address set to $ipv4") if $ipv4; success("IPv6 address set to $ipv6") if $ipv6; @@ -6181,9 +6133,9 @@ sub nic_yandex_update { failed("%s", $response->{error}); next; } - $config{$host}{'ip'} = $ip; - $config{$host}{'mtime'} = $now; - $config{$host}{'status'} = 'good'; + $recap{$host}{'ip'} = $ip; + $recap{$host}{'mtime'} = $now; + $recap{$host}{'status'} = 'good'; success("updated successfully to $ip"); } } @@ -6247,11 +6199,11 @@ sub nic_duckdns_update { next; } for my $h (@hosts) { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; - $config{$h}{'status-ipv4'} = 'good' if $ipv4; - $config{$h}{'status-ipv6'} = 'good' if $ipv6; + $recap{$h}{'ipv4'} = $ipv4 if $ipv4; + $recap{$h}{'ipv6'} = $ipv6 if $ipv6; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status-ipv4'} = 'good' if $ipv4; + $recap{$h}{'status-ipv6'} = 'good' if $ipv6; } success("IPv4 address set to $ipv4") if $ipv4; success("IPv6 address set to $ipv6") if $ipv6; @@ -6302,9 +6254,9 @@ sub nic_freemyip_update { failed("server said: $body"); next; } - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } } @@ -6352,9 +6304,9 @@ sub nic_ddnsfm_update { url => opt('server', $h) . "/update?key=" . opt('password', $h) . "&domain=$h&myip=$ip", ); next if !header_ok($reply); - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-ipv$ipv"} = 'good'; success("IPv$ipv address set to $ip"); } } @@ -6401,9 +6353,9 @@ sub nic_dondominio_update { failed("server said: $returned"); next; } - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } } @@ -6465,9 +6417,9 @@ sub nic_dnsmadeeasy_update { failed("server said: $err"); next; } - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } } @@ -6533,16 +6485,16 @@ sub nic_ovh_update { my @reply = split /\n/, $reply; my $returned = List::Util::first { $_ =~ /good/ || $_ =~ /nochg/ } @reply; if ($returned) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; if ($returned =~ /good/) { success("IP address set to $ip"); } else { success("skipped: IP address was already set to $ip"); } } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; failed("server said: $reply"); } } @@ -6678,7 +6630,7 @@ sub nic_porkbun_update { warning("There are multiple applicable records. Only first record is used. Overwrite all with the same content.") if @$records > 1; if ($records->[0]{'content'} eq $ip) { - $config{$h}{"status-ipv$ipv"} = "good"; + $recap{$h}{"status-ipv$ipv"} = "good"; success("skipped: IPv$ipv address was already set to $ip"); next; } @@ -6700,7 +6652,7 @@ sub nic_porkbun_update { }), ); next if !header_ok($reply); - $config{$h}{"status-ipv$ipv"} = "good"; + $recap{$h}{"status-ipv$ipv"} = "good"; success("IPv$ipv address set to $ip"); } } @@ -6755,14 +6707,14 @@ sub nic_cloudns_update { $reply =~ s/^.*?\n\n//s; # Strip the headers. chomp($reply); if ($reply eq "The record's key is wrong!" || $reply eq "Invalid request.") { - $config{$_}{'status'} = 'failed' for @hosts; + $recap{$_}{'status'} = 'failed' for @hosts; failed($reply); next; } # There's no documentation explaining possible return values, so we assume success. - $config{$_}{'ip'} = $ip for @hosts; - $config{$_}{'mtime'} = $now for @hosts; - $config{$_}{'status'} = 'good' for @hosts; + $recap{$_}{'ip'} = $ip for @hosts; + $recap{$_}{'mtime'} = $now for @hosts; + $recap{$_}{'status'} = 'good' for @hosts; success("IP address set to $ip"); } } @@ -6811,7 +6763,7 @@ sub nic_dinahosting_update { password => opt('password', $h), url => $url, ); - $config{$h}{'status'} = 'failed'; # assume failure until otherwise determined + $recap{$h}{'status'} = 'failed'; # assume failure until otherwise determined next if !header_ok($reply); $reply =~ s/^.*?\n\n//s; # Strip the headers. if ($reply !~ /Success/i) { @@ -6822,9 +6774,9 @@ sub nic_dinahosting_update { failed("error $code: $message"); next; } - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } } @@ -6876,20 +6828,20 @@ sub nic_directnic_update { (my $body = $reply) =~ s/^.*?\n\n//s; my $response = eval {decode_json($body)}; if (ref($response) ne 'HASH') { - $config{$h}{"status-ipv$ipv"} = 'bad'; + $recap{$h}{"status-ipv$ipv"} = 'bad'; failed("response is not a JSON object:\n$body"); next; } if ($response->{'result'} ne 'success') { - $config{$h}{"status-ipv$ipv"} = 'failed'; + $recap{$h}{"status-ipv$ipv"} = 'failed'; failed("server said:\n$body"); next; } - $config{$h}{"ipv$ipv"} = $ip; - $config{$h}{"status-ipv$ipv"} = 'good'; - $config{$h}{'mtime'} = $now; + $recap{$h}{"ipv$ipv"} = $ip; + $recap{$h}{"status-ipv$ipv"} = 'good'; + $recap{$h}{'mtime'} = $now; success("IPv$ipv address set to $ip"); } } @@ -6972,15 +6924,15 @@ sub nic_gandi_update { $reply =~ s/^.*?\n\n//s; my $response = eval { decode_json($reply) }; if (ref($response) ne 'HASH') { - $config{$h}{"status-$ipv"} = "bad"; + $recap{$h}{"status-$ipv"} = "bad"; failed("response is not a JSON object: $reply"); next; } if ($response->{'rrset_values'}->[0] eq $ip && (!defined(opt('ttl', $h)) || $response->{'rrset_ttl'} eq opt('ttl', $h))) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-$ipv"} = "good"; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-$ipv"} = "good"; success("skipped: address was already set to $ip"); next; } @@ -6995,7 +6947,7 @@ sub nic_gandi_update { }), ); if (!header_ok($reply)) { - $config{$h}{"status-$ipv"} = "bad"; + $recap{$h}{"status-$ipv"} = "bad"; $reply =~ s/^.*?\n\n//s; my $response = eval { decode_json($reply) }; if (ref($response) eq 'HASH' && ($response->{message} // '') ne '') { @@ -7005,9 +6957,9 @@ sub nic_gandi_update { } next; } - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-$ipv"} = "good"; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-$ipv"} = "good"; success("updated successfully to $ip"); } } @@ -7055,12 +7007,12 @@ sub nic_keysystems_update { last if !header_ok($reply); if ($reply =~ /code = 200/) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; failed("server said: $reply"); } } @@ -7109,11 +7061,11 @@ sub nic_regfishde_update { failed("server said: $reply"); next; } - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'status-ipv4'} = 'good' if $ipv4; - $config{$h}{'status-ipv6'} = 'good' if $ipv6; - $config{$h}{'mtime'} = $now; + $recap{$h}{'ipv4'} = $ipv4 if $ipv4; + $recap{$h}{'ipv6'} = $ipv6 if $ipv6; + $recap{$h}{'status-ipv4'} = 'good' if $ipv4; + $recap{$h}{'status-ipv6'} = 'good' if $ipv6; + $recap{$h}{'mtime'} = $now; success("IPv4 address set to $ipv4") if $ipv4; success("IPv6 address set to $ipv6") if $ipv6; } @@ -7183,12 +7135,12 @@ sub nic_enom_update { my @reply = split /\n/, $reply; if (grep /Done=true/i, @reply) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; + $recap{$h}{'ip'} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{'status'} = 'good'; success("IP address set to $ip"); } else { - $config{$h}{'status'} = 'failed'; + $recap{$h}{'status'} = 'failed'; warning("SENT: %s", $url) unless opt('verbose'); warning("REPLIED: %s", $reply); failed("invalid reply"); @@ -7252,7 +7204,7 @@ sub nic_digitalocean_update_one { my $list = eval { decode_json($list_resp) }; if ($@) { - $config{$h}{"status-$ipv"} = 'failed'; + $recap{$h}{"status-$ipv"} = 'failed'; failed("listing $ipv: JSON decoding failure"); return; } @@ -7261,7 +7213,7 @@ sub nic_digitalocean_update_one { unless ((ref($elem) eq 'HASH') && (ref ($elem = $elem->{'domain_records'}) eq 'ARRAY') && (@$elem == 1 && ref ($elem = $elem->[0]) eq 'HASH')) { - $config{$h}{"status-$ipv"} = 'failed'; + $recap{$h}{"status-$ipv"} = 'failed'; failed("listing $ipv: no record, multiple records, or malformed JSON"); return; } @@ -7283,9 +7235,9 @@ sub nic_digitalocean_update_one { return if !header_ok($update_resp); } - $config{$h}{"status-$ipv"} = 'good'; - $config{$h}{"ip-$ipv"} = $ip; - $config{$h}{"mtime"} = $now; + $recap{$h}{"status-$ipv"} = 'good'; + $recap{$h}{"ip-$ipv"} = $ip; + $recap{$h}{"mtime"} = $now; } sub nic_digitalocean_update { @@ -7393,9 +7345,9 @@ sub nic_infomaniak_update { next; } success($msg); - $config{$h}{"ipv$v"} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{"status-ipv$v"} = 'good'; + $recap{$h}{"ipv$v"} = $ip; + $recap{$h}{'mtime'} = $now; + $recap{$h}{"status-ipv$v"} = 'good'; } } } @@ -7417,11 +7369,11 @@ sub nic_emailonly_update { logmsg(email => 1, raw => 1, join("\n", 'Host IP addresses:', map({ my $ipv4 = delete($config{$_}{'wantipv4'}); my $ipv6 = delete($config{$_}{'wantipv6'}); - $config{$_}{'status-ipv4'} = 'good' if $ipv4; - $config{$_}{'status-ipv6'} = 'good' if $ipv6; - $config{$_}{'ipv4'} = $ipv4 if $ipv4; - $config{$_}{'ipv6'} = $ipv6 if $ipv6; - $config{$_}{'mtime'} = $now; + $recap{$_}{'status-ipv4'} = 'good' if $ipv4; + $recap{$_}{'status-ipv6'} = 'good' if $ipv6; + $recap{$_}{'ipv4'} = $ipv4 if $ipv4; + $recap{$_}{'ipv6'} = $ipv6 if $ipv6; + $recap{$_}{'mtime'} = $now; sprintf('%30s %s', $_, join(' ', grep(defined($_), $ipv4, $ipv6))); } @_))); } diff --git a/t/protocol_directnic.pl b/t/protocol_directnic.pl index d6c8e54..fc24a28 100644 --- a/t/protocol_directnic.pl +++ b/t/protocol_directnic.pl @@ -52,7 +52,7 @@ my @test_cases = ( { desc => 'IPv4, good', cfg => {h1 => {urlv4 => "$hostname/dns/gateway/abc/", wantipv4 => '192.0.2.1'}}, - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, }, wantlogs => [ @@ -62,7 +62,7 @@ my @test_cases = ( { desc => 'IPv4, failed', cfg => {h1 => {urlv4 => "$hostname/dns/gateway/bad_token/", wantipv4 => '192.0.2.1'}}, - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'failed'}, }, wantlogs => [ @@ -72,7 +72,7 @@ my @test_cases = ( { desc => 'IPv4, bad', cfg => {h1 => {urlv4 => "$hostname/bad/path/", wantipv4 => '192.0.2.1'}}, - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'bad'}, }, wantlogs => [ @@ -82,7 +82,7 @@ my @test_cases = ( { desc => 'IPv4, unexpected response', cfg => {h1 => {urlv4 => "$hostname/unexpected/path/", wantipv4 => '192.0.2.1'}}, - wantstatus => {h1 => {}}, + wantrecap => {}, wantlogs => [ {label => 'FAILED', ctx => ['h1'], msg => qr/400 Bad Request/}, ], @@ -90,7 +90,7 @@ my @test_cases = ( { desc => 'IPv4, no urlv4', cfg => {h1 => {wantipv4 => '192.0.2.1'}}, - wantstatus => {h1 => {}}, + wantrecap => {}, wantlogs => [ {label => 'FAILED', ctx => ['h1'], msg => qr/missing urlv4 option/}, ], @@ -98,7 +98,7 @@ my @test_cases = ( { desc => 'IPv6, good', cfg => {h1 => {urlv6 => "$hostname/dns/gateway/abc/", wantipv6 => '2001:db8::1'}}, - wantstatus => { + wantrecap => { h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now}, }, wantlogs => [ @@ -113,7 +113,7 @@ my @test_cases = ( wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1', }}, - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now}, @@ -132,7 +132,7 @@ my @test_cases = ( wantipv6 => '2001:db8::1', }}, wantips => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}}, - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'failed', 'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now}, @@ -152,20 +152,14 @@ for my $tc (@test_cases) { local $ddclient::globals{verbose} = 1; my $l = Logger->new($ddclient::_l); local %ddclient::config = %{$tc->{cfg}}; + local %ddclient::recap; { local $ddclient::_l = $l; ddclient::nic_directnic_update(sort(keys(%{$tc->{cfg}}))); } - # These are the properties in %ddclient::config to check against $tc->{wantstatus}. - my %statuskeys = map(($_ => undef), qw(atime ip ipv4 ipv6 mtime status status-ipv4 status-ipv6 - wantip wantipv4 wantipv6 wtime)); - my %gotstatus; - for my $h (keys(%ddclient::config)) { - $gotstatus{$h} = {map(($_ => $ddclient::config{$h}{$_}), - grep(exists($statuskeys{$_}), keys(%{$ddclient::config{$h}})))}; - } - is_deeply(\%gotstatus, $tc->{wantstatus}, "$tc->{desc}: status") - or diag(ddclient::repr(\%ddclient::config, Names => ['*ddclient::config'])); + is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap") + or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}], + Names => ['*got', '*want'])); $tc->{wantlogs} //= []; subtest("$tc->{desc}: logs" => sub { my @got = @{$l->{logs}}; diff --git a/t/protocol_dyndns2.pl b/t/protocol_dyndns2.pl index c0d407d..cd79658 100644 --- a/t/protocol_dyndns2.pl +++ b/t/protocol_dyndns2.pl @@ -45,7 +45,7 @@ my @test_cases = ( cfg => {h1 => {wantipv4 => '192.0.2.1'}}, resp => ['good'], wantquery => 'hostname=h1&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, }, wantlogs => [ @@ -57,7 +57,7 @@ my @test_cases = ( cfg => {h1 => {wantipv4 => '192.0.2.1'}}, resp => ['nochg'], wantquery => 'hostname=h1&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, }, wantlogs => [ @@ -70,7 +70,7 @@ my @test_cases = ( cfg => {h1 => {wantipv4 => '192.0.2.1'}}, resp => ['nohost'], wantquery => 'hostname=h1&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'nohost'}, }, wantlogs => [ @@ -82,7 +82,7 @@ my @test_cases = ( cfg => {h1 => {wantipv4 => '192.0.2.1'}}, resp => ['WAT'], wantquery => 'hostname=h1&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'WAT'}, }, wantlogs => [ @@ -100,7 +100,7 @@ my @test_cases = ( 'good', ], wantquery => 'hostname=h1,h2&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, }, @@ -122,7 +122,7 @@ my @test_cases = ( 'dnserr', ], wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, h3 => {'status-ipv4' => 'dnserr'}, @@ -139,7 +139,7 @@ my @test_cases = ( cfg => {h1 => {wantipv6 => '2001:db8::1'}}, resp => ['good'], wantquery => 'hostname=h1&myip=2001:db8::1', - wantstatus => { + wantrecap => { h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now}, }, wantlogs => [ @@ -151,7 +151,7 @@ my @test_cases = ( cfg => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}}, resp => ['good'], wantquery => 'hostname=h1&myip=192.0.2.1,2001:db8::1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now}, @@ -173,7 +173,7 @@ my @test_cases = ( 'WAT', ], wantquery => 'hostname=h1,h2&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, }, @@ -191,7 +191,7 @@ my @test_cases = ( }, resp => ['abuse'], wantquery => 'hostname=h1,h2&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'abuse'}, h2 => {'status-ipv4' => 'abuse'}, }, @@ -208,7 +208,7 @@ my @test_cases = ( }, resp => ['good'], wantquery => 'hostname=h1,h2&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, }, @@ -230,7 +230,7 @@ my @test_cases = ( 'nochg', ], wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1', - wantstatus => { + wantrecap => { h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now}, h3 => {'status-ipv4' => 'unknown'}, @@ -252,6 +252,7 @@ for my $tc (@test_cases) { local $ddclient::globals{verbose} = 1; my $l = Logger->new($ddclient::_l); local %ddclient::config; + local %ddclient::recap; $ddclient::config{$_} = { login => 'username', password => 'password', @@ -267,18 +268,9 @@ for my $tc (@test_cases) { local $ddclient::_l = $l; ddclient::nic_dyndns2_update(sort(keys(%{$tc->{cfg}}))); } - # These are the properties in %ddclient::config to check against $tc->{wantstatus}. Keys are - # explicitly listed here rather than read from $tc->{wantstatus} to ensure that entries that - # should not exist (e.g., wantipv4 and friends) are deleted (or never set). - my %statuskeys = map(($_ => undef), qw(atime ip ipv4 ipv6 mtime status status-ipv4 status-ipv6 - wantip wantipv4 wantipv6 wtime)); - my %gotstatus; - for my $h (keys(%ddclient::config)) { - $gotstatus{$h} = {map(($_ => $ddclient::config{$h}{$_}), - grep(exists($statuskeys{$_}), keys(%{$ddclient::config{$h}})))}; - } - is_deeply(\%gotstatus, $tc->{wantstatus}, "$tc->{desc}: status") - or diag(ddclient::repr(\%ddclient::config, Names => ['*ddclient::config'])); + is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap") + or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}], + Names => ['*got', '*want'])); $tc->{wantlogs} //= []; subtest("$tc->{desc}: logs" => sub { my @got = @{$l->{logs}}; diff --git a/t/read_recap.pl b/t/read_recap.pl index b601eae..923d971 100644 --- a/t/read_recap.pl +++ b/t/read_recap.pl @@ -1,6 +1,5 @@ use Test::More; use File::Temp; -use Scalar::Util qw(refaddr); SKIP: { eval { require Test::Warnings; } or skip($@, 1); } eval { require 'ddclient'; } or BAIL_OUT($@); @@ -9,42 +8,26 @@ local $ddclient::globals{verbose} = 1; local %ddclient::protocols = ( protocol_a => { variables => { - 'host' => {type => ddclient::T_STRING(), recap => 1}, - 'mtime' => {type => ddclient::T_NUMBER(), recap => 1}, - 'atime' => {type => ddclient::T_NUMBER(), recap => 1}, - 'wtime' => {type => ddclient::T_NUMBER(), recap => 1}, - 'ip' => {type => ddclient::T_IP(), recap => 1}, - 'ipv4' => {type => ddclient::T_IPV4(), recap => 1}, - 'ipv6' => {type => ddclient::T_IPV6(), recap => 1}, - 'status' => {type => ddclient::T_ANY(), recap => 1}, - 'status-ipv4' => {type => ddclient::T_ANY(), recap => 1}, - 'status-ipv6' => {type => ddclient::T_ANY(), recap => 1}, - 'warned-min-error-interval' => {type => ddclient::T_ANY(), recap => 1}, - 'warned-min-interval' => {type => ddclient::T_ANY(), recap => 1}, - - 'var_a' => {type => ddclient::T_BOOL(), recap => 1}, + host => {type => ddclient::T_STRING(), recap => 1}, + var_a => {type => ddclient::T_BOOL(), recap => 1}, }, }, protocol_b => { variables => { - 'host' => {type => ddclient::T_STRING(), recap => 1}, - 'mtime' => {type => ddclient::T_NUMBER()}, # Intentionally not a recap var. - 'var_b' => {type => ddclient::T_NUMBER(), recap => 1}, + host => {type => ddclient::T_STRING(), recap => 1}, + var_b => {type => ddclient::T_NUMBER(), recap => 1}, + var_b_non_recap => {type => ddclient::T_ANY()}, }, }, ); local %ddclient::variables = (merged => {map({ %{$ddclient::protocols{$_}{variables}}; } sort(keys(%ddclient::protocols)))}); -# Sentinel value that means "this hash entry should be deleted." -my $DOES_NOT_EXIST = []; - my @test_cases = ( { desc => "ok value", cachefile_lines => ["var_a=yes host_a"], want => {host_a => {host => 'host_a', var_a => 1}}, - # No config changes are expected because `var_a` is not a "status" recap var. }, { desc => "unknown host", @@ -71,80 +54,16 @@ my @test_cases = ( host_a => {host => 'host_a', var_a => 1}, host_b => {host => 'host_b', var_b => 123}, }, - # No config changes are expected because `var_a` and `var_b` are not "status" recap vars. }, { - desc => "used to be status vars", - cachefile_lines => ["ip=192.0.2.1,status=good host_a"], - want => {host_a => {host => 'host_a', ip => '192.0.2.1', status => 'good'}}, - # No config changes are expected because `ip` and `status` are no longer "status" recap - # vars. - }, - { - desc => "status vars", - cachefile_lines => ["mtime=1234567890,atime=1234567891,wtime=1234567892,ipv4=192.0.2.1,ipv6=2001:db8::1,status-ipv4=good,status-ipv6=bad,warned-min-interval=1234567893,warned-min-error-interval=1234567894 host_a"], - want => {host_a => { - 'host' => 'host_a', - 'mtime' => 1234567890, - 'atime' => 1234567891, - 'wtime' => 1234567892, - 'ipv4' => '192.0.2.1', - 'ipv6' => '2001:db8::1', - 'status-ipv4' => 'good', - 'status-ipv6' => 'bad', - 'warned-min-interval' => 1234567893, - 'warned-min-error-interval' => 1234567894, - }}, - want_config_changes => {host_a => { - 'mtime' => 1234567890, - 'atime' => 1234567891, - 'wtime' => 1234567892, - 'ipv4' => '192.0.2.1', - 'ipv6' => '2001:db8::1', - 'status-ipv4' => 'good', - 'status-ipv6' => 'bad', - 'warned-min-interval' => 1234567893, - 'warned-min-error-interval' => 1234567894, - }}, - }, - { - desc => "unset status var clears config", - cachefile_lines => ["host_a"], - config => {host_a => { - 'mtime' => 1234567890, - 'atime' => 1234567891, - 'wtime' => 1234567892, - 'ipv4' => '192.0.2.1', - 'ipv6' => '2001:db8::1', - 'status-ipv4' => 'good', - 'status-ipv6' => 'bad', - 'warned-min-interval' => 1234567893, - 'warned-min-error-interval' => 1234567894, - 'var_a' => 1, - }}, - want => {host_a => {host => 'host_a'}}, - want_config_changes => {host_a => { - 'mtime' => $DOES_NOT_EXIST, - 'atime' => $DOES_NOT_EXIST, - 'wtime' => $DOES_NOT_EXIST, - 'ipv4' => $DOES_NOT_EXIST, - 'ipv6' => $DOES_NOT_EXIST, - 'status-ipv4' => $DOES_NOT_EXIST, - 'status-ipv6' => $DOES_NOT_EXIST, - 'warned-min-interval' => $DOES_NOT_EXIST, - 'warned-min-error-interval' => $DOES_NOT_EXIST, - # `var_a` should remain untouched. - }}, - }, - { - desc => "non-recap vars are not loaded to %recap or copied to %config", - cachefile_lines => ["mtime=1234567890 host_b"], + desc => "non-recap vars are not loaded to %recap", + cachefile_lines => ["var_b_non_recap=foo host_b"], want => {host_b => {host => 'host_b'}}, }, { desc => "non-recap vars are scrubbed from %recap", - cachefile_lines => ["mtime=1234567890 host_b"], - recap => {host_b => {host => 'host_b', mtime => 1234567891}}, + cachefile_lines => ["var_b_non_recap=foo host_b"], + recap => {host_b => {host => 'host_b', var_b_non_recap => 'foo'}}, want => {host_b => {host => 'host_b'}}, }, { @@ -166,8 +85,6 @@ for my $tc (@test_cases) { host_a => {protocol => 'protocol_a'}, host_b => {protocol => 'protocol_b'}, ); - $tc->{config} //= {}; - $want_config{$_} = {%{$want_config{$_} // {}}, %{$tc->{config}{$_}}} for keys(%{$tc->{config}}); # Deep clone %want_config so we can check for changes. local %ddclient::config; $ddclient::config{$_} = {%{$want_config{$_}}} for keys(%want_config); @@ -180,21 +97,9 @@ for my $tc (@test_cases) { or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{want}], Names => ['*got', '*want'])); } - TODO: { - local $TODO = $tc->{want_config_changes_TODO}; - $tc->{want_config_changes} //= {}; - $want_config{$_} = {%{$want_config{$_} // {}}, %{$tc->{want_config_changes}{$_}}} - for keys(%{$tc->{want_config_changes}}); - for my $h (keys(%want_config)) { - for my $k (keys(%{$want_config{$h}})) { - my $a = refaddr($want_config{$h}{$k}); - delete($want_config{$h}{$k}) if defined($a) && $a == refaddr($DOES_NOT_EXIST); - } - } - is_deeply(\%ddclient::config, \%want_config, "$tc->{desc}: %config") - or diag(ddclient::repr(Values => [\%ddclient::config, \%want_config], - Names => ['*got', '*want'])); - } + is_deeply(\%ddclient::config, \%want_config, "$tc->{desc}: %config") + or diag(ddclient::repr(Values => [\%ddclient::config, \%want_config], + Names => ['*got', '*want'])); } done_testing(); diff --git a/t/update_nics.pl b/t/update_nics.pl index c249abd..85d2126 100644 --- a/t/update_nics.pl +++ b/t/update_nics.pl @@ -54,9 +54,9 @@ local %ddclient::protocols = ( local $ddclient::_l = ddclient::pushlogctx($h); ddclient::debug('updating host'); push(@updates, [@_]); - $ddclient::config{$h}{status} = 'good'; - $ddclient::config{$h}{ip} = delete($ddclient::config{$h}{wantip}); - $ddclient::config{$h}{mtime} = $ddclient::now; + $ddclient::recap{$h}{status} = 'good'; + $ddclient::recap{$h}{ip} = delete($ddclient::config{$h}{wantip}); + $ddclient::recap{$h}{mtime} = $ddclient::now; } ddclient::debug('returning from update'); }), @@ -83,12 +83,6 @@ my @test_cases = ( 'mtime' => $ddclient::now, 'status-ipv4' => 'good', }, - want_cfg_changes => { - 'atime' => $ddclient::now, - 'ipv4' => '192.0.2.1', - 'mtime' => $ddclient::now, - 'status-ipv4' => 'good', - }, %$_, }; } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), @@ -107,12 +101,6 @@ my @test_cases = ( 'mtime' => $ddclient::now, 'status-ipv6' => 'good', }, - want_cfg_changes => { - 'atime' => $ddclient::now, - 'ipv6' => '2001:db8::1', - 'mtime' => $ddclient::now, - 'status-ipv6' => 'good', - }, }, { desc => 'legacy, fresh, usev6=webv6', @@ -128,12 +116,6 @@ my @test_cases = ( 'mtime' => $ddclient::now, 'status-ipv6' => 'good', }, - want_cfg_changes => { - 'atime' => $ddclient::now, - 'ipv6' => '2001:db8::1', - 'mtime' => $ddclient::now, - 'status-ipv6' => 'good', - }, }, { desc => 'legacy, fresh, usev4=webv4 usev6=webv6', @@ -150,12 +132,6 @@ my @test_cases = ( 'mtime' => $ddclient::now, 'status-ipv4' => 'good', }, - want_cfg_changes => { - 'atime' => $ddclient::now, - 'ipv4' => '192.0.2.1', - 'mtime' => $ddclient::now, - 'status-ipv4' => 'good', - }, }, map({ my %cfg = %{delete($_->{cfg})}; @@ -211,9 +187,6 @@ my @test_cases = ( want_recap_changes => { 'warned-min-interval' => $ddclient::now, }, - want_cfg_changes => { - 'warned-min-interval' => $ddclient::now, - }, %$_, }; } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), @@ -238,11 +211,6 @@ my @test_cases = ( 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now, }, - want_cfg_changes => { - 'atime' => $ddclient::now, - 'ipv4' => '192.0.2.1', - 'mtime' => $ddclient::now, - }, %$_, }; } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), @@ -265,9 +233,6 @@ my @test_cases = ( want_recap_changes => { 'warned-min-error-interval' => $ddclient::now, }, - want_cfg_changes => { - 'warned-min-error-interval' => $ddclient::now, - }, %$_, }; } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), @@ -293,12 +258,6 @@ my @test_cases = ( 'mtime' => $ddclient::now, 'status-ipv4' => 'good', }, - want_cfg_changes => { - 'atime' => $ddclient::now, - 'ipv4' => '192.0.2.1', - 'mtime' => $ddclient::now, - 'status-ipv4' => 'good', - }, %$_, }; } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), @@ -317,7 +276,6 @@ for my $tc (@test_cases) { # $cachef is an object that stringifies to a filename. local $ddclient::globals{cache} = "$cachef"; my %cfg = ( - %{$tc->{recap} // {}}, # Simulate a previous update. web => 'v4', webv4 => 'v4', webv6 => 'v6',