Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
@@ -0,0 +1,721 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#########################################################################################
|
||||
#### hetzner-dns-manager.sh — Manage DNS zones and records via the Hetzner DNS API ####
|
||||
#### List zones, add/update/delete records, BIND export/import, audit, bulk ops ####
|
||||
#### Requires: bash 4+, curl, jq ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### License: MIT ####
|
||||
#### Version 1.01 ####
|
||||
#### ####
|
||||
#### Usage: ####
|
||||
#### ./hetzner-dns-manager.sh --zones ####
|
||||
#### ####
|
||||
#### See --help for all options. ####
|
||||
#########################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors (pre-initialized) ─────────────────────────────────────────
|
||||
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
||||
|
||||
setup_colors() {
|
||||
if [[ "${COLOR:-auto}" == "never" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ "${COLOR:-auto}" == "always" ]] || [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
RESET='\033[0m'
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────
|
||||
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
||||
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||||
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
section_header() {
|
||||
echo ""
|
||||
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
field() {
|
||||
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
|
||||
}
|
||||
|
||||
field_color() {
|
||||
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
|
||||
}
|
||||
|
||||
elapsed() {
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
echo "$(( end_time - START_TIME ))s"
|
||||
}
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
RUN_MODE=""
|
||||
ZONE_NAME=""
|
||||
ZONE_ID=""
|
||||
RECORD_ID=""
|
||||
RECORD_TYPE=""
|
||||
RECORD_NAME=""
|
||||
RECORD_VALUE=""
|
||||
RECORD_TTL="3600"
|
||||
CSV_FILE=""
|
||||
OUTPUT_FORMAT="${HDM_FORMAT:-table}"
|
||||
FORCE="false"
|
||||
VERBOSE="${VERBOSE:-false}"
|
||||
COLOR="${COLOR:-auto}"
|
||||
IMPORT_FILE=""
|
||||
EXPORT_FILE=""
|
||||
|
||||
# ── Credentials ───────────────────────────────────────────────────────
|
||||
HETZNER_DNS_TOKEN="${HETZNER_DNS_TOKEN:-}"
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
readonly SCRIPT_NAME
|
||||
START_TIME=""
|
||||
ACTION_OK=0
|
||||
ACTION_FAIL=0
|
||||
|
||||
# ── API helpers ──────────────────────────────────────────────────────
|
||||
hdns_api() {
|
||||
local method="$1" endpoint="$2"
|
||||
shift 2
|
||||
local attempt=0 max_attempts=3
|
||||
|
||||
while (( attempt < max_attempts )); do
|
||||
local http_code
|
||||
http_code=$(curl -s -o /tmp/hdm_resp.json -w "%{http_code}" \
|
||||
-X "$method" \
|
||||
-H "Auth-API-Token: ${HETZNER_DNS_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://dns.hetzner.com/api/v1${endpoint}" "$@")
|
||||
|
||||
verbose "API ${method} ${endpoint} → HTTP ${http_code}"
|
||||
|
||||
if [[ "$http_code" == "429" ]]; then
|
||||
((attempt++)) || true
|
||||
local wait=$(( attempt * 5 ))
|
||||
warn "Rate limited — retrying in ${wait}s (attempt ${attempt}/${max_attempts})"
|
||||
sleep "$wait"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$http_code" =~ ^[45] ]]; then
|
||||
local errmsg
|
||||
errmsg=$(jq -r '.error.message // .message // empty' /tmp/hdm_resp.json 2>/dev/null)
|
||||
[[ -n "$errmsg" ]] && verbose "API error: ${errmsg}"
|
||||
fi
|
||||
|
||||
cat /tmp/hdm_resp.json
|
||||
return 0
|
||||
done
|
||||
|
||||
err "API request failed after ${max_attempts} attempts: ${method} ${endpoint}"
|
||||
return 1
|
||||
}
|
||||
|
||||
hdns_api_raw() {
|
||||
local method="$1" endpoint="$2"
|
||||
shift 2
|
||||
|
||||
curl -s \
|
||||
-X "$method" \
|
||||
-H "Auth-API-Token: ${HETZNER_DNS_TOKEN}" \
|
||||
"https://dns.hetzner.com/api/v1${endpoint}" "$@"
|
||||
}
|
||||
|
||||
check_credentials() {
|
||||
[[ -z "$HETZNER_DNS_TOKEN" ]] && die "HETZNER_DNS_TOKEN not set"
|
||||
}
|
||||
|
||||
check_deps() {
|
||||
command -v curl &>/dev/null || die "curl is required"
|
||||
command -v jq &>/dev/null || die "jq is required"
|
||||
}
|
||||
|
||||
# ── Zone helpers ─────────────────────────────────────────────────────
|
||||
resolve_zone_id() {
|
||||
local name="$1"
|
||||
local resp
|
||||
resp=$(hdns_api GET "/zones?name=${name}")
|
||||
local zid
|
||||
zid=$(echo "$resp" | jq -r '.zones[0].id // empty' 2>/dev/null)
|
||||
if [[ -z "$zid" ]]; then
|
||||
die "Zone not found: ${name}"
|
||||
fi
|
||||
echo "$zid"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# ZONES
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_zones() {
|
||||
local page=1 per_page=100 all_data="[]"
|
||||
|
||||
while true; do
|
||||
local resp
|
||||
resp=$(hdns_api GET "/zones?page=${page}&per_page=${per_page}")
|
||||
local page_data
|
||||
page_data=$(echo "$resp" | jq '.zones // []' 2>/dev/null)
|
||||
local page_count
|
||||
page_count=$(echo "$page_data" | jq 'length' 2>/dev/null || echo 0)
|
||||
[[ "$page_count" -eq 0 ]] && break
|
||||
all_data=$(echo -e "${all_data}\n${page_data}" | jq -s 'add' 2>/dev/null)
|
||||
(( page_count < per_page )) && break
|
||||
((page++)) || true
|
||||
done
|
||||
|
||||
local total
|
||||
total=$(echo "$all_data" | jq 'length' 2>/dev/null || echo 0)
|
||||
[[ "$total" -eq 0 ]] && die "No zones found"
|
||||
|
||||
case "$OUTPUT_FORMAT" in
|
||||
json)
|
||||
echo "$all_data" | jq '.'
|
||||
;;
|
||||
prometheus)
|
||||
cat <<EOF
|
||||
# HELP hetzner_dns_zones_total Total DNS zones
|
||||
# TYPE hetzner_dns_zones_total gauge
|
||||
hetzner_dns_zones_total ${total}
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
section_header "DNS Zones"
|
||||
|
||||
printf " ${BOLD}%-34s %-18s %-10s %-8s %-8s${RESET}\n" \
|
||||
"ZONE_ID" "NAME" "STATUS" "RECORDS" "TTL"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..80})"
|
||||
|
||||
echo "$all_data" | jq -r \
|
||||
'.[] | "\(.id)\t\(.name // "unknown")\t\(.status // "—")\t\(.records_count // 0)\t\(.ttl // 0)"' \
|
||||
2>/dev/null \
|
||||
| while IFS=$'\t' read -r zid name status rcount ttl; do
|
||||
printf " %-34s %-18s %-10s %-8s %-8s\n" \
|
||||
"${zid:0:32}" "${name:0:16}" "$status" "$rcount" "$ttl"
|
||||
done
|
||||
|
||||
echo ""
|
||||
field "Zones:" "$total"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# RECORDS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_records() {
|
||||
[[ -z "$ZONE_NAME" ]] && die "Specify --zone DOMAIN"
|
||||
|
||||
local zid
|
||||
zid=$(resolve_zone_id "$ZONE_NAME")
|
||||
|
||||
local resp
|
||||
resp=$(hdns_api GET "/records?zone_id=${zid}")
|
||||
local records
|
||||
records=$(echo "$resp" | jq '.records // []' 2>/dev/null)
|
||||
local total
|
||||
total=$(echo "$records" | jq 'length' 2>/dev/null || echo 0)
|
||||
|
||||
case "$OUTPUT_FORMAT" in
|
||||
json)
|
||||
echo "$records" | jq '.'
|
||||
;;
|
||||
prometheus)
|
||||
cat <<EOF
|
||||
# HELP hetzner_dns_records_total Total DNS records
|
||||
# TYPE hetzner_dns_records_total gauge
|
||||
hetzner_dns_records_total ${total}
|
||||
# HELP hetzner_dns_zone_records DNS records per zone
|
||||
# TYPE hetzner_dns_zone_records gauge
|
||||
hetzner_dns_zone_records{zone="${ZONE_NAME}"} ${total}
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
section_header "DNS Records — ${ZONE_NAME}"
|
||||
|
||||
printf " ${BOLD}%-34s %-6s %-18s %-26s %-6s${RESET}\n" \
|
||||
"RECORD_ID" "TYPE" "NAME" "VALUE" "TTL"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..92})"
|
||||
|
||||
echo "$records" | jq -r \
|
||||
'.[] | "\(.id)\t\(.type)\t\(.name // "@")\t\(.value // "—")\t\(.ttl // 0)"' \
|
||||
2>/dev/null \
|
||||
| while IFS=$'\t' read -r rid rtype rname rvalue rttl; do
|
||||
printf " %-34s %-6s %-18s %-26s %-6s\n" \
|
||||
"${rid:0:32}" "$rtype" "${rname:0:16}" "${rvalue:0:24}" "$rttl"
|
||||
done
|
||||
|
||||
echo ""
|
||||
field "Records:" "$total"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# ADD
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_add() {
|
||||
[[ -z "$ZONE_NAME" ]] && die "Specify --zone DOMAIN"
|
||||
[[ -z "$RECORD_TYPE" ]] && die "Specify --type TYPE"
|
||||
[[ -z "$RECORD_NAME" ]] && die "Specify --name NAME"
|
||||
[[ -z "$RECORD_VALUE" ]] && die "Specify --value VALUE"
|
||||
|
||||
local zid
|
||||
zid=$(resolve_zone_id "$ZONE_NAME")
|
||||
|
||||
local payload
|
||||
payload=$(jq -n \
|
||||
--arg zid "$zid" \
|
||||
--arg type "$RECORD_TYPE" \
|
||||
--arg name "$RECORD_NAME" \
|
||||
--arg value "$RECORD_VALUE" \
|
||||
--argjson ttl "$RECORD_TTL" \
|
||||
'{zone_id: $zid, type: $type, name: $name, value: $value, ttl: $ttl}')
|
||||
|
||||
local resp
|
||||
resp=$(hdns_api POST "/records" -d "$payload")
|
||||
|
||||
local rid
|
||||
rid=$(echo "$resp" | jq -r '.record.id // empty' 2>/dev/null)
|
||||
|
||||
if [[ -n "$rid" ]]; then
|
||||
echo -e " ${GREEN}✓${RESET} Record created: ${RECORD_TYPE} ${RECORD_NAME} → ${RECORD_VALUE} (ID: ${rid})"
|
||||
((ACTION_OK++)) || true
|
||||
else
|
||||
local errmsg
|
||||
errmsg=$(echo "$resp" | jq -r '.error.message // .message // "unknown error"' 2>/dev/null)
|
||||
echo -e " ${RED}✗${RESET} Failed to create record: ${errmsg}"
|
||||
((ACTION_FAIL++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# UPDATE
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_update() {
|
||||
[[ -z "$RECORD_ID" ]] && die "Specify --record-id ID"
|
||||
[[ -z "$ZONE_NAME" ]] && die "Specify --zone DOMAIN"
|
||||
[[ -z "$RECORD_TYPE" ]] && die "Specify --type TYPE"
|
||||
[[ -z "$RECORD_NAME" ]] && die "Specify --name NAME"
|
||||
[[ -z "$RECORD_VALUE" ]] && die "Specify --value VALUE"
|
||||
|
||||
local zid
|
||||
zid=$(resolve_zone_id "$ZONE_NAME")
|
||||
|
||||
local payload
|
||||
payload=$(jq -n \
|
||||
--arg zid "$zid" \
|
||||
--arg type "$RECORD_TYPE" \
|
||||
--arg name "$RECORD_NAME" \
|
||||
--arg value "$RECORD_VALUE" \
|
||||
--argjson ttl "$RECORD_TTL" \
|
||||
'{zone_id: $zid, type: $type, name: $name, value: $value, ttl: $ttl}')
|
||||
|
||||
local resp
|
||||
resp=$(hdns_api PUT "/records/${RECORD_ID}" -d "$payload")
|
||||
|
||||
local rid
|
||||
rid=$(echo "$resp" | jq -r '.record.id // empty' 2>/dev/null)
|
||||
|
||||
if [[ -n "$rid" ]]; then
|
||||
echo -e " ${GREEN}✓${RESET} Record updated: ${RECORD_TYPE} ${RECORD_NAME} → ${RECORD_VALUE} (ID: ${rid})"
|
||||
((ACTION_OK++)) || true
|
||||
else
|
||||
local errmsg
|
||||
errmsg=$(echo "$resp" | jq -r '.error.message // .message // "unknown error"' 2>/dev/null)
|
||||
echo -e " ${RED}✗${RESET} Failed to update record: ${errmsg}"
|
||||
((ACTION_FAIL++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# DELETE
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_delete() {
|
||||
[[ -z "$RECORD_ID" ]] && die "Specify --record-id ID"
|
||||
[[ "$FORCE" != "true" ]] && die "Delete is destructive — use --force to confirm"
|
||||
|
||||
local resp
|
||||
resp=$(hdns_api DELETE "/records/${RECORD_ID}")
|
||||
|
||||
echo -e " ${GREEN}✓${RESET} Record deleted: ${RECORD_ID}"
|
||||
((ACTION_OK++)) || true
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# EXPORT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_export() {
|
||||
[[ -z "$ZONE_NAME" ]] && die "Specify --zone DOMAIN"
|
||||
|
||||
local zid
|
||||
zid=$(resolve_zone_id "$ZONE_NAME")
|
||||
|
||||
local zone_data
|
||||
zone_data=$(hdns_api_raw GET "/zones/${zid}/export")
|
||||
|
||||
if [[ -n "$EXPORT_FILE" ]]; then
|
||||
echo "$zone_data" > "$EXPORT_FILE"
|
||||
echo -e " ${GREEN}✓${RESET} Zone exported to ${EXPORT_FILE}"
|
||||
else
|
||||
echo "$zone_data"
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# IMPORT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_import() {
|
||||
[[ -z "$ZONE_NAME" ]] && die "Specify --zone DOMAIN"
|
||||
[[ -z "$IMPORT_FILE" ]] && die "Specify --file FILE"
|
||||
[[ ! -f "$IMPORT_FILE" ]] && die "File not found: ${IMPORT_FILE}"
|
||||
[[ "$FORCE" != "true" ]] && die "Import replaces ALL zone records — use --force to confirm"
|
||||
|
||||
local zid
|
||||
zid=$(resolve_zone_id "$ZONE_NAME")
|
||||
|
||||
local zone_data
|
||||
zone_data=$(cat "$IMPORT_FILE")
|
||||
|
||||
local http_code
|
||||
http_code=$(curl -s -o /tmp/hdm_resp.json -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Auth-API-Token: ${HETZNER_DNS_TOKEN}" \
|
||||
-H "Content-Type: text/plain" \
|
||||
"https://dns.hetzner.com/api/v1/zones/${zid}/import" \
|
||||
--data-binary "$zone_data")
|
||||
|
||||
if [[ "$http_code" =~ ^2 ]]; then
|
||||
echo -e " ${GREEN}✓${RESET} Zone imported from ${IMPORT_FILE}"
|
||||
((ACTION_OK++)) || true
|
||||
else
|
||||
local errmsg
|
||||
errmsg=$(jq -r '.error.message // .message // "unknown error"' /tmp/hdm_resp.json 2>/dev/null)
|
||||
echo -e " ${RED}✗${RESET} Import failed: ${errmsg}"
|
||||
((ACTION_FAIL++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# BULK ADD
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_bulk_add() {
|
||||
[[ -z "$ZONE_NAME" ]] && die "Specify --zone DOMAIN"
|
||||
[[ -z "$CSV_FILE" ]] && die "Specify --csv FILE"
|
||||
[[ ! -f "$CSV_FILE" ]] && die "CSV file not found: ${CSV_FILE}"
|
||||
|
||||
local zid
|
||||
zid=$(resolve_zone_id "$ZONE_NAME")
|
||||
|
||||
section_header "Bulk Add — ${ZONE_NAME}"
|
||||
|
||||
local line_num=0
|
||||
while IFS=',' read -r rtype rname rvalue rttl; do
|
||||
((line_num++)) || true
|
||||
[[ -z "$rtype" || "$rtype" =~ ^# ]] && continue
|
||||
rtype=$(echo "$rtype" | xargs)
|
||||
rname=$(echo "$rname" | xargs)
|
||||
rvalue=$(echo "$rvalue" | xargs)
|
||||
rttl=$(echo "${rttl:-3600}" | xargs)
|
||||
|
||||
local payload
|
||||
payload=$(jq -n \
|
||||
--arg zid "$zid" \
|
||||
--arg type "$rtype" \
|
||||
--arg name "$rname" \
|
||||
--arg value "$rvalue" \
|
||||
--argjson ttl "$rttl" \
|
||||
'{zone_id: $zid, type: $type, name: $name, value: $value, ttl: $ttl}')
|
||||
|
||||
local resp
|
||||
resp=$(hdns_api POST "/records" -d "$payload")
|
||||
|
||||
local rid
|
||||
rid=$(echo "$resp" | jq -r '.record.id // empty' 2>/dev/null)
|
||||
|
||||
if [[ -n "$rid" ]]; then
|
||||
echo -e " ${GREEN}✓${RESET} ${rtype} ${rname} → ${rvalue} (line ${line_num})"
|
||||
((ACTION_OK++)) || true
|
||||
else
|
||||
echo -e " ${RED}✗${RESET} ${rtype} ${rname} → ${rvalue} (line ${line_num})"
|
||||
((ACTION_FAIL++)) || true
|
||||
fi
|
||||
|
||||
sleep 0.5
|
||||
done < "$CSV_FILE"
|
||||
|
||||
echo ""
|
||||
field_color "Succeeded:" "${GREEN}${ACTION_OK}${RESET}"
|
||||
if [[ "$ACTION_FAIL" -gt 0 ]]; then
|
||||
field_color "Failed:" "${RED}${ACTION_FAIL}${RESET}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# AUDIT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_audit() {
|
||||
[[ -z "$ZONE_NAME" ]] && die "Specify --zone DOMAIN"
|
||||
|
||||
local zid
|
||||
zid=$(resolve_zone_id "$ZONE_NAME")
|
||||
|
||||
local resp
|
||||
resp=$(hdns_api GET "/records?zone_id=${zid}")
|
||||
local records
|
||||
records=$(echo "$resp" | jq '.records // []' 2>/dev/null)
|
||||
local total
|
||||
total=$(echo "$records" | jq 'length' 2>/dev/null || echo 0)
|
||||
|
||||
local warnings=0
|
||||
|
||||
if [[ "$OUTPUT_FORMAT" != "prometheus" ]]; then
|
||||
section_header "DNS Audit — ${ZONE_NAME}"
|
||||
field "Records:" "$total"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check SOA
|
||||
local soa_count
|
||||
soa_count=$(echo "$records" | jq '[.[] | select(.type == "SOA")] | length' 2>/dev/null || echo 0)
|
||||
if [[ "$soa_count" -eq 0 ]]; then
|
||||
((warnings++)) || true
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${YELLOW}⚠${RESET} No SOA record found"
|
||||
else
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${GREEN}✓${RESET} SOA record present"
|
||||
fi
|
||||
|
||||
# Check NS
|
||||
local ns_count
|
||||
ns_count=$(echo "$records" | jq '[.[] | select(.type == "NS")] | length' 2>/dev/null || echo 0)
|
||||
if [[ "$ns_count" -eq 0 ]]; then
|
||||
((warnings++)) || true
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${RED}✗${RESET} No NS records found"
|
||||
elif [[ "$ns_count" -lt 2 ]]; then
|
||||
((warnings++)) || true
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${YELLOW}⚠${RESET} Only ${ns_count} NS record(s) — recommend at least 2"
|
||||
else
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${GREEN}✓${RESET} ${ns_count} NS records"
|
||||
fi
|
||||
|
||||
# Check common types
|
||||
for rtype in A AAAA MX TXT; do
|
||||
local rcount
|
||||
rcount=$(echo "$records" | jq --arg t "$rtype" '[.[] | select(.type == $t)] | length' 2>/dev/null || echo 0)
|
||||
if [[ "$rcount" -eq 0 ]]; then
|
||||
((warnings++)) || true
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${YELLOW}⚠${RESET} No ${rtype} records found"
|
||||
else
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${GREEN}✓${RESET} ${rcount} ${rtype} record(s)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check low TTLs
|
||||
local low_ttl
|
||||
low_ttl=$(echo "$records" | jq '[.[] | select(.ttl < 300 and .ttl > 0)] | length' 2>/dev/null || echo 0)
|
||||
if [[ "$low_ttl" -gt 0 ]]; then
|
||||
((warnings++)) || true
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${YELLOW}⚠${RESET} ${low_ttl} record(s) with TTL < 300s"
|
||||
else
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${GREEN}✓${RESET} All TTLs ≥ 300s"
|
||||
fi
|
||||
|
||||
# Check wildcards
|
||||
local wildcard
|
||||
wildcard=$(echo "$records" | jq '[.[] | select(.name | startswith("*"))] | length' 2>/dev/null || echo 0)
|
||||
if [[ "$wildcard" -gt 0 ]]; then
|
||||
[[ "$OUTPUT_FORMAT" != "prometheus" ]] && echo -e " ${CYAN}ℹ${RESET} ${wildcard} wildcard record(s)"
|
||||
fi
|
||||
|
||||
if [[ "$OUTPUT_FORMAT" == "prometheus" ]]; then
|
||||
cat <<EOF
|
||||
# HELP hetzner_dns_audit_warnings DNS audit warnings
|
||||
# TYPE hetzner_dns_audit_warnings gauge
|
||||
hetzner_dns_audit_warnings{zone="${ZONE_NAME}"} ${warnings}
|
||||
EOF
|
||||
return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$warnings" -eq 0 ]]; then
|
||||
field_color "Warnings:" "${GREEN}0${RESET}"
|
||||
else
|
||||
field_color "Warnings:" "${YELLOW}${warnings}${RESET}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# HELP
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
${BOLD}${SCRIPT_NAME}${RESET} — Hetzner DNS Manager
|
||||
|
||||
Manage DNS zones and records via the Hetzner DNS API.
|
||||
|
||||
${BOLD}MODES${RESET}
|
||||
--zones List all DNS zones
|
||||
--records List records for a zone (requires --zone)
|
||||
--add Add a DNS record
|
||||
--update Update a DNS record (requires --record-id)
|
||||
--delete Delete a DNS record (requires --record-id, --force)
|
||||
--export Export zone in BIND format
|
||||
--import Import zone from BIND file (requires --file, --force)
|
||||
--bulk-add Bulk add records from CSV (requires --csv)
|
||||
--audit Audit zone for common issues
|
||||
|
||||
${BOLD}TARGETING${RESET}
|
||||
--zone DOMAIN Target zone by domain name (resolved to ID internally)
|
||||
--record-id ID Target a specific record by ID
|
||||
|
||||
${BOLD}RECORD FIELDS${RESET}
|
||||
--type TYPE Record type (A, AAAA, CNAME, MX, TXT, SRV, etc.)
|
||||
--name NAME Record name — subdomain part (e.g., "www", "@" for apex)
|
||||
--value VALUE Record value (e.g., IP address, hostname)
|
||||
--ttl TTL TTL in seconds (default: 3600)
|
||||
|
||||
${BOLD}OPTIONS${RESET}
|
||||
--format FMT Output: table, json, prometheus (default: table)
|
||||
--csv FILE CSV file for bulk add (type,name,value,ttl)
|
||||
--file FILE BIND zone file for import
|
||||
--output FILE Write export to file instead of stdout
|
||||
--force Required for delete and import operations
|
||||
--verbose Debug output
|
||||
--no-color Disable colored output
|
||||
--help Show this help message
|
||||
|
||||
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
||||
HETZNER_DNS_TOKEN Hetzner DNS API token (required, separate from HCLOUD_TOKEN)
|
||||
HDM_FORMAT Default output format
|
||||
VERBOSE Enable verbose output (true/false)
|
||||
COLOR Color mode: auto, always, never
|
||||
|
||||
${BOLD}EXAMPLES${RESET}
|
||||
# List all zones
|
||||
${SCRIPT_NAME} --zones
|
||||
|
||||
# List records for a zone
|
||||
${SCRIPT_NAME} --records --zone example.com
|
||||
|
||||
# Add an A record
|
||||
${SCRIPT_NAME} --add --zone example.com --type A --name www --value 168.119.10.50
|
||||
|
||||
# Add MX record with custom TTL
|
||||
${SCRIPT_NAME} --add --zone example.com --type MX --name @ --value "10 mail.example.com" --ttl 7200
|
||||
|
||||
# Update a record
|
||||
${SCRIPT_NAME} --update --record-id abc123 --zone example.com --type A --name www --value 168.119.10.51
|
||||
|
||||
# Delete a record
|
||||
${SCRIPT_NAME} --delete --record-id abc123 --force
|
||||
|
||||
# Export zone as BIND file
|
||||
${SCRIPT_NAME} --export --zone example.com --output example.com.zone
|
||||
|
||||
# Import BIND zone file
|
||||
${SCRIPT_NAME} --import --zone example.com --file example.com.zone --force
|
||||
|
||||
# Bulk add from CSV
|
||||
${SCRIPT_NAME} --bulk-add --zone example.com --csv records.csv
|
||||
|
||||
# Audit zone
|
||||
${SCRIPT_NAME} --audit --zone example.com
|
||||
|
||||
# Prometheus metrics
|
||||
${SCRIPT_NAME} --zones --format prometheus
|
||||
|
||||
${BOLD}EXIT CODES${RESET}
|
||||
0 Success
|
||||
1 Runtime error
|
||||
EOF
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# PARSE ARGS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--zones) RUN_MODE="zones"; shift ;;
|
||||
--records) RUN_MODE="records"; shift ;;
|
||||
--add) RUN_MODE="add"; shift ;;
|
||||
--update) RUN_MODE="update"; shift ;;
|
||||
--delete) RUN_MODE="delete"; shift ;;
|
||||
--export) RUN_MODE="export"; shift ;;
|
||||
--import) RUN_MODE="import"; shift ;;
|
||||
--bulk-add) RUN_MODE="bulk-add"; shift ;;
|
||||
--audit) RUN_MODE="audit"; shift ;;
|
||||
--zone) ZONE_NAME="${2:?--zone requires a DOMAIN}"; shift 2 ;;
|
||||
--record-id) RECORD_ID="${2:?--record-id requires an ID}"; shift 2 ;;
|
||||
--type) RECORD_TYPE="${2:?--type requires a TYPE}"; shift 2 ;;
|
||||
--name) RECORD_NAME="${2:?--name requires a NAME}"; shift 2 ;;
|
||||
--value) RECORD_VALUE="${2:?--value requires a VALUE}"; shift 2 ;;
|
||||
--ttl) RECORD_TTL="${2:?--ttl requires a TTL}"; shift 2 ;;
|
||||
--csv) CSV_FILE="${2:?--csv requires a FILE}"; shift 2 ;;
|
||||
--file) IMPORT_FILE="${2:?--file requires a FILE}"; shift 2 ;;
|
||||
--output) EXPORT_FILE="${2:?--output requires a FILE}"; shift 2 ;;
|
||||
--format) OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
|
||||
--force) FORCE="true"; shift ;;
|
||||
--verbose) VERBOSE="true"; shift ;;
|
||||
--no-color) COLOR="never"; shift ;;
|
||||
--help|-h) setup_colors; show_help; exit 0 ;;
|
||||
*) die "Unknown option: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# MAIN
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
main() {
|
||||
parse_args "$@"
|
||||
setup_colors
|
||||
|
||||
if [[ -z "$RUN_MODE" ]]; then
|
||||
err "No mode specified"
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_deps
|
||||
check_credentials
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
case "$RUN_MODE" in
|
||||
zones) do_zones ;;
|
||||
records) do_records ;;
|
||||
add) do_add ;;
|
||||
update) do_update ;;
|
||||
delete) do_delete ;;
|
||||
export) do_export ;;
|
||||
import) do_import ;;
|
||||
bulk-add) do_bulk_add ;;
|
||||
audit) do_audit ;;
|
||||
*) die "Unknown mode: ${RUN_MODE}" ;;
|
||||
esac
|
||||
|
||||
if [[ "$OUTPUT_FORMAT" != "prometheus" ]]; then
|
||||
echo ""
|
||||
field "Duration:" "$(elapsed)"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user