summaryrefslogtreecommitdiff
path: root/hetzner-ddns
blob: 0c78fa01913d0074e05e376a8ef59516cb1aa82b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env bash
#
# Copyright 2022 David Vazgenovich Shakaryan
#
# HETZNER_TOKEN=<token> hetzner-ddns <domain>
# HETZNER_TOKEN_FILE=/path/to/token hetzner-ddns <domain>
# systemctl enable --now "hetzner-ddns@$(systemd-escape <domain>).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}"