system-manager-wakasu
1#!/usr/bin/env bash
2# Update Gandi DNS records from NixOS DNS configuration
3# Usage: ./scripts/update-gandi-dns.sh [--dry-run]
4
5set -euo pipefail
6
7# Colors
8BOLD='\033[1m'
9GREEN='\033[0;32m'
10BLUE='\033[0;34m'
11YELLOW='\033[0;33m'
12RED='\033[0;31m'
13CYAN='\033[0;36m'
14RESET='\033[0m'
15
16DOMAIN="sbr.pm"
17API_URL="https://api.gandi.net/v5/livedns/domains/$DOMAIN/records"
18DRY_RUN=false
19
20# Parse arguments
21for arg in "$@"; do
22 case $arg in
23 --dry-run)
24 DRY_RUN=true
25 shift
26 ;;
27 esac
28done
29
30# Check for API key (using lego's environment variable name)
31if [[ -z "${GANDIV5_PERSONAL_TOKEN:-}" ]]; then
32 echo -e "${RED}Error: GANDIV5_PERSONAL_TOKEN environment variable not set${RESET}"
33 echo -e "${YELLOW}Please set it with: export GANDIV5_PERSONAL_TOKEN=your-api-key${RESET}"
34 echo -e "${CYAN}Or source it from the agenix secret on rhea:${RESET}"
35 echo -e "${CYAN} source /run/agenix/gandi.env${RESET}"
36 exit 1
37fi
38
39echo -e "${BOLD}${BLUE}Updating Gandi DNS records for $DOMAIN...${RESET}"
40if [[ "$DRY_RUN" == "true" ]]; then
41 echo -e "${YELLOW}DRY RUN MODE - No changes will be made${RESET}"
42fi
43echo
44
45# Get the DNS zone file content from Nix
46echo -e "${CYAN}Extracting DNS records from Nix configuration...${RESET}"
47ZONE_FILE=$(nix eval --raw '.#nixosConfigurations.demeter.config.services.bind.zones."sbr.pm".file' 2>&1 | \
48 grep -v "^warning:" | grep -v "^Using saved setting")
49
50if [[ -z "$ZONE_FILE" ]]; then
51 echo -e "${RED}Error: Could not generate zone file${RESET}"
52 exit 1
53fi
54
55echo -e "${GREEN}DNS records extracted${RESET}"
56echo
57
58# Create a temporary file for the zone
59TEMP_ZONE=$(mktemp)
60echo "$ZONE_FILE" > "$TEMP_ZONE"
61
62# Extract A records (excluding SOA, NS, and comments)
63# Format examples:
64# name.domain. IN A ip
65# name.domain. TTL IN A ip
66# *.name.domain. IN A ip
67RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?A\s+" "$TEMP_ZONE" | \
68 grep -v "SOA" | \
69 grep -v "^;" || true)
70
71rm -f "$TEMP_ZONE"
72
73if [[ -z "$RECORDS" ]]; then
74 echo -e "${YELLOW}No A records found in zone file${RESET}"
75 exit 0
76fi
77
78echo -e "${GREEN}Found $(echo "$RECORDS" | wc -l) A records to process${RESET}"
79echo
80
81# Get current DNS records from Gandi
82if [[ "$DRY_RUN" == "false" ]]; then
83 echo -e "${CYAN}Fetching current DNS records from Gandi...${RESET}"
84 CURRENT_RECORDS=$(curl -s \
85 -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
86 "$API_URL" || echo "[]")
87
88 echo -e "${GREEN}Current records fetched${RESET}"
89 echo
90fi
91
92# Process each record
93UPDATED=0
94SKIPPED=0
95FAILED=0
96
97while IFS= read -r line; do
98 # Parse the line to extract: name, TTL, and value
99 # Handle various formats:
100 # name.sbr.pm. IN A value
101 # name.sbr.pm. TTL IN A value
102 # *.name.sbr.pm. TTL IN A value
103
104 # Extract components
105 FULL_NAME=$(echo "$line" | awk '{print $1}')
106
107 # Check if second field is a number (TTL) or "IN"
108 SECOND_FIELD=$(echo "$line" | awk '{print $2}')
109 if [[ "$SECOND_FIELD" =~ ^[0-9]+$ ]]; then
110 TTL="$SECOND_FIELD"
111 VALUE=$(echo "$line" | awk '{print $5}')
112 else
113 TTL=10800
114 VALUE=$(echo "$line" | awk '{print $4}')
115 fi
116
117 # Remove .sbr.pm. suffix and convert to Gandi format
118 NAME="${FULL_NAME%.sbr.pm.}"
119
120 # Convert wildcard format: *.name -> *.name (Gandi uses this format)
121 # Convert root wildcard: * -> @ (Gandi's root wildcard)
122 if [[ "$NAME" == "*" ]]; then
123 NAME="@"
124 fi
125
126 echo -e "${BLUE}Processing: ${RESET}$NAME.$DOMAIN A $VALUE (TTL: $TTL)"
127
128 if [[ "$DRY_RUN" == "true" ]]; then
129 echo -e " ${CYAN}[DRY RUN] Would update/create record${RESET}"
130 UPDATED=$((UPDATED + 1))
131 else
132 # Check if record exists and has same value
133 CURRENT_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
134 --arg name "$NAME" \
135 --arg type "A" \
136 '.[] | select(.rrset_name == $name and .rrset_type == $type) | .rrset_values[0]' \
137 2>/dev/null || echo "")
138
139 if [[ "$CURRENT_VALUE" == "$VALUE" ]]; then
140 echo -e " ${GREEN}✓ Record unchanged, skipping${RESET}"
141 SKIPPED=$((SKIPPED + 1))
142 else
143 if [[ -z "$CURRENT_VALUE" ]]; then
144 echo -e " ${YELLOW}Creating new record...${RESET}"
145 else
146 echo -e " ${YELLOW}Updating record (was: $CURRENT_VALUE)...${RESET}"
147 fi
148
149 # Update/create the record
150 RESPONSE=$(curl -s -w "\n%{http_code}" \
151 -X PUT \
152 -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
153 -H "Content-Type: application/json" \
154 -d "{\"rrset_values\": [\"$VALUE\"], \"rrset_ttl\": $TTL}" \
155 "$API_URL/$NAME/A")
156
157 HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
158 BODY=$(echo "$RESPONSE" | sed '$d')
159
160 if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
161 echo -e " ${GREEN}✓ Record updated successfully${RESET}"
162 UPDATED=$((UPDATED + 1))
163 else
164 echo -e " ${RED}✗ Failed to update record (HTTP $HTTP_CODE)${RESET}"
165 echo -e " ${RED}Response: $BODY${RESET}"
166 FAILED=$((FAILED + 1))
167 fi
168 fi
169 fi
170
171 echo
172done <<< "$RECORDS"
173
174echo -e "${BOLD}${GREEN}DNS update complete!${RESET}"
175echo
176echo -e "${CYAN}Summary:${RESET}"
177echo -e " Updated: $UPDATED"
178echo -e " Skipped (unchanged): $SKIPPED"
179if [[ $FAILED -gt 0 ]]; then
180 echo -e " ${RED}Failed: $FAILED${RESET}"
181fi