summaryrefslogtreecommitdiff
path: root/hetzner-ddns
diff options
context:
space:
mode:
Diffstat (limited to 'hetzner-ddns')
-rwxr-xr-xhetzner-ddns68
1 files changed, 42 insertions, 26 deletions
diff --git a/hetzner-ddns b/hetzner-ddns
index 6d70e69..0c78fa0 100755
--- a/hetzner-ddns
+++ b/hetzner-ddns
@@ -6,63 +6,79 @@
# HETZNER_TOKEN_FILE=/path/to/token hetzner-ddns <domain>
# systemctl enable --now "hetzner-ddns@$(systemd-escape <domain>).timer"
-IP_RESOLVER='https://ifconfig.co'
+IP_RESOLVER='https://ip.hetzner.com'
TARGET="${1}"
shopt -s extglob
die() {
- [[ -n "${@}" ]] && echo "${@}" >&2
+ [[ -n "${@}" ]] && echo "${0##*/}: ${@}" >&2
exit 1
}
hetzcurl() {
- curl -sfH "Auth-API-Token: ${HETZNER_TOKEN}" \
- "https://dns.hetzner.com/api/v1/${1}" \
+ curl -sfm 30 --connect-timeout 10 \
+ -H "Authorization: Bearer ${HETZNER_TOKEN}" \
+ "https://api.hetzner.cloud/v1/${1}" \
"${@:2}"
}
if [[ -z "${HETZNER_TOKEN}" ]] && [[ -n "${HETZNER_TOKEN_FILE}" ]]; then
- [[ -f "${HETZNER_TOKEN_FILE}" ]] || die 'Specified token file' \
- "(${HETZNER_TOKEN_FILE}) does not exist"
+ [[ -f "${HETZNER_TOKEN_FILE}" ]] || die \
+ "specified token file does not exist: ${HETZNER_TOKEN_FILE}"
HETZNER_TOKEN="$(<"${HETZNER_TOKEN_FILE}")"
fi
-[[ -n "${HETZNER_TOKEN}" ]] || die 'Missing token'
+[[ -n "${HETZNER_TOKEN}" ]] || die 'no token specified'
-ip="$(curl -sf4 "${IP_RESOLVER}")" || die 'IP lookup failed'
+ip="$(curl -sf4m 30 --connect-timeout 10 "${IP_RESOLVER}")" || die \
+ 'IP lookup failed'
+res="$(hetzcurl "zones")" || die 'zones lookup failed'
zone_re="${TARGET}"
while [[ "${zone_re}" =~ ^([^\\]*)\.(.*)$ ]]; do
zone_re="(${BASH_REMATCH[1]}\\.)?${BASH_REMATCH[2]}"
done
-
-res="$(hetzcurl "zones")" || die 'Zones lookup failed'
IFS='|' read zone_id zone_name < <(jq -er --arg re "${zone_re}" \
'[.zones[] | select(.name | test("\\A" + $re + "\\z"))] |
if . == [] then (null | halt_error) else . end |
max_by(.name | length) | [.id, .name] | join("|")' \
- <<< "${res}") || die 'Zone not found'
+ <<< "${res}") || die 'zone not found'
rec_name="${TARGET%%?(.)${zone_name}}"
rec_name="${rec_name:-@}"
-res="$(hetzcurl "records?zone_id=${zone_id}")" || die 'Records lookup failed'
-rec_id="$(jq -er --arg name "${rec_name}" \
- 'first(.records[] | select(.type == "A" and .name == $name)) | .id' \
- <<< "${res}")" || die 'Record not found'
-
-res="$(hetzcurl "records/${rec_id}")" || die 'Record lookup failed'
-old_ip="$(jq -r '.record.value' <<< "${res}")"
-
+res="$(hetzcurl "zones/${zone_id}/rrsets/${rec_name}/A")" || die \
+ 'records lookup failed'
+old_ip="$(jq -er '.rrset.records[0].value' <<< "${res}")" || die \
+ 'record not found'
if [[ "${old_ip}" == "${ip}" ]]; then
echo "IP unchanged from ${ip}"
exit
fi
-data="$(jq -c --arg ip "${ip}" \
- '.record | {zone_id, type, name, value: $ip }' <<< "${res}")"
-res="$(hetzcurl "records/${rec_id}" \
- -H 'Content-Type: application/json' \
- -X PUT -d "${data}")" || die 'Record update failed'
-new_ip="$(jq -r '.record.value' <<< "${res}")"
+task="IP change from ${old_ip} to ${ip}"
+data="$(jq -c --arg ip "${ip}" --arg name "${0##*/}" \
+ '{records: [{value: $ip, comment:
+ $name + (now | strftime(" %Y-%m-%d %H:%M:%S"))}]}' \
+ <<< "${res}")"
+res="$(hetzcurl "zones/${zone_id}/rrsets/${rec_name}/A/actions/set_records" \
+ -H 'Content-Type: application/json' -X POST -d "${data}")" || die \
+ "record update failed for ${task}"
+action_id="$(jq -r '.action.id' <<< "${res}")"
+task="${task} (action id: ${action_id})"
+c=0
+while true; do
+ status="$(jq -r '.action.status' <<< "${res}")"
+ case "${status}" in
+ 'success')
+ break ;;
+ 'running')
+ ((++c > 16)) && die "timed out confirming ${task}"
+ sleep "$((c > 8 ? 4 : 1))" ;;
+ *)
+ die "record update ${status:-failed} for ${task}" ;;
+ esac
-echo "IP changed from ${old_ip} to ${new_ip}"
+ res="$(hetzcurl "actions/${action_id}")" || die \
+ "action lookup failed for ${task}"
+done
+echo "Completed ${task}"