diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2026-05-13 00:13:57 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2026-05-16 18:42:27 +0300 |
| commit | b5860daf11ac353049cb1654b9414a129e5cfb96 (patch) | |
| tree | 87ed89711e4f0e85ace0a97fa123199152c67302 /roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 | |
| parent | 4715a28fdcd87440400d17154bfa361d99db29cc (diff) | |
Rework
Diffstat (limited to 'roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2')
| -rw-r--r-- | roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 b/roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 new file mode 100644 index 0000000..a2f4430 --- /dev/null +++ b/roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 @@ -0,0 +1,130 @@ +#!/bin/sh +# +# Hetzner DNS record updater (one-shot for cron) +# Requirements: curl, awk + +set -eu + +# ---------------------------- Defaults --------------------------------------- +# Env-overridable: +: "${API_TOKEN_FILE:=/usr/local/etc/hetzner_auth}" +: "${API_BASE:=https://api.hetzner.cloud/v1}" +# ----------------------------------------------------------------------------- + +# Defaults +TTL=300 + +usage() { + cat <<'USAGE' >&2 +Usage: hetzner_ddns.sh [OPTIONS] + +Options (named): + --zone NAME_OR_ID Hetzner Zone name or ID (primary mode only) + --rr-id ID RRSet identifier in the form "rr-name/rr-type" (e.g., "host/A") + --iface-cmd CMD Command that prints interface info (for IP discovery) + --ttl TTL Time-to-live of the record (optional) + -h, --help Show this help + +Environment: + API_TOKEN_FILE Path to file containing ONLY the API token + (default: /usr/local/etc/hetzner_auth) + API_BASE Hetzner Cloud DNS API base URL + (default: https://api.hetzner.cloud/v1) + +Examples: + API_TOKEN_FILE=/secret/token \ + ./hetzner_ddns.sh \ + --zone example.com --rr-name host --rr-type A \ + --iface-cmd "ifconfig em0" +USAGE +} + +ts() { date +"%Y-%m-%dT%H:%M:%S%z"; } +fail() { echo "$(ts) ERROR: $*" >&2; exit 2; } +error() { echo "$(ts) ERROR: $*" >&2; } +warn() { echo "$(ts) WARN: $*" >&2; } +info() { echo "$(ts) INFO: $*"; } + +# ----------------------------- Arg parsing ----------------------------------- +# Accept both `--key value` and `--key=value` + +while [ $# -gt 0 ]; do + case "$1" in + --zone=*) ZONE=${1#*=} ;; + --zone) ZONE=$2; shift ;; + --rr-id=*) RR_ID=${1#*=} ;; + --rr-id) RR_ID=$2; shift ;; + --iface-cmd=*) IFACE_CMD=${1#*=} ;; + --iface-cmd) IFACE_CMD=$2; shift ;; + --ttl=*) TTL=${1#*=} ;; + --ttl) TTL=$2; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; break ;; + -*) + error "Unknown option: $1" + usage + exit 2 + ;; + *) + error "Unexpected positional argument: $1" + usage + exit 2 + ;; + esac + shift +done + +# ----------------------------- Validation ------------------------------------ +[ -n "${ZONE:-}" ] || fail "Missing --zone" +[ -n "${RR_ID:-}" ] || fail "Missing --rr-id" +[ -n "${IFACE_CMD:-}" ] || fail "Missing --iface-cmd" + +# ------------------------------ Auth ----------------------------------------- +if [ ! -r "${API_TOKEN_FILE}" ]; then + fail "Token file missing or unreadable: ${API_TOKEN_FILE}" +fi +API_TOKEN="$(cat "${API_TOKEN_FILE}" | tr -d '[:space:]')" +[ -n "${API_TOKEN}" ] || fail "API token is empty" + +# ------------------------------ Helpers -------------------------------------- +get_ip() { + # Extract first IPv4 after 'inet ' (ignore inet6) + sh -c "${IFACE_CMD}" 2>/dev/null | awk '/(^|[[:space:]])inet[[:space:]]/ {print $2; exit}' +} + +# ------------------------------ Main ----------------------------------------- +IFACE_IP="$(get_ip || true)" +if [ -z "${IFACE_IP}" ]; then + warn "No IPv4 address found via '${IFACE_CMD}' (interface not ready?)" + exit 1 +fi + +BODY=$(printf '{"records":[{"value":"%s","comment":"Updated by hetzner_ddns.sh at %s","ttl":"%s"}]}' \ + "${IFACE_IP}" "$(ts)" "${TTL}") + +HTTP_CODE=$( + curl -sS -o /dev/null -w "%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${API_TOKEN}" \ + --data "${BODY}" \ + "${API_BASE}/zones/${ZONE}/rrsets/${RR_ID}/actions/set_records" +) + +case "${HTTP_CODE}" in + 2*) + info "Updated zone ${ZONE} RRSet ${RR_ID} -> ${IFACE_IP}" + exit 0 + ;; + 4*) + error "Client error from API (HTTP ${HTTP_CODE}) - check zone/rr params/token/body" + exit 2 + ;; + 5*) + warn "Server error from API (HTTP ${HTTP_CODE})" + exit 1 + ;; + *) + warn "Unexpected HTTP status ${HTTP_CODE}" + exit 1 + ;; +esac |
