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