#!/usr/bin/env bash # # Copyright 2022 David Vazgenovich Shakaryan # # HETZNER_TOKEN= hetzner-ddns # HETZNER_TOKEN_FILE=/path/to/token hetzner-ddns # systemctl enable --now "hetzner-ddns@$(systemd-escape ).timer" IP_RESOLVER='https://ip.hetzner.com' TARGET="${1}" shopt -s extglob die() { [[ -n "${@}" ]] && echo "${0##*/}: ${@}" >&2 exit 1 } hetzcurl() { 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 does not exist: ${HETZNER_TOKEN_FILE}" HETZNER_TOKEN="$(<"${HETZNER_TOKEN_FILE}")" fi [[ -n "${HETZNER_TOKEN}" ]] || die 'no token specified' 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 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' rec_name="${TARGET%%?(.)${zone_name}}" rec_name="${rec_name:-@}" 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 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 res="$(hetzcurl "actions/${action_id}")" || die \ "action lookup failed for ${task}" done echo "Completed ${task}"