aboutsummaryrefslogtreecommitdiffstats
path: root/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2
blob: eec12d7edfbd0c27f940605ebfd3e991f559e160 (plain)
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/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}"
# -----------------------------------------------------------------------------


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)
  -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 ;;
    -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"}]}' \
              "${IFACE_IP}" "$(ts)")

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/update_records"
)

case "${HTTP_CODE}" in
  200)
    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