first commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
|||||||
|
# Cloudflare DNS Updater
|
||||||
|
|
||||||
|
A Bash script that automatically updates Cloudflare DNS A records with your current public IP address. This script demonstrates various advanced Bash programming features while providing a practical tool for dynamic DNS management.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic IP Detection**: Retrieves current public IP using external services
|
||||||
|
- **Multi-Domain Support**: Manages DNS records for multiple domains simultaneously
|
||||||
|
- **Smart Updates**: Only updates records when IP changes to avoid unnecessary API calls
|
||||||
|
- **Orphaned Record Cleanup**: Automatically removes unmanaged DNS records
|
||||||
|
- **Protected Records**: Prevents deletion of critical DNS records
|
||||||
|
- **Dry Run Mode**: Test changes without actually modifying DNS records
|
||||||
|
- **Comprehensive Logging**: Detailed output showing all operations performed
|
||||||
|
|
||||||
|
## Bash Features Demonstrated
|
||||||
|
|
||||||
|
This script showcases the following Bash programming features:
|
||||||
|
|
||||||
|
### 1. **Shebang and Script Structure**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
```
|
||||||
|
Proper script initialization with bash interpreter specification.
|
||||||
|
|
||||||
|
### 2. **Variable Declarations and Constants**
|
||||||
|
```bash
|
||||||
|
TOKEN=""
|
||||||
|
AUTO_DELETE_ORPHANS=true
|
||||||
|
DRY_RUN=false
|
||||||
|
```
|
||||||
|
Use of string constants and boolean flags for configuration.
|
||||||
|
|
||||||
|
### 3. **Associative Arrays**
|
||||||
|
```bash
|
||||||
|
declare -A d0 d1 # Declare associative arrays for domain configuration
|
||||||
|
d0=(
|
||||||
|
["domain"]=""
|
||||||
|
["zone_id"]=""
|
||||||
|
["subdomains"]="@,"
|
||||||
|
["protected_records"]=""
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Key-value data structures for complex domain configurations.
|
||||||
|
|
||||||
|
### 4. **Indexed Arrays**
|
||||||
|
```bash
|
||||||
|
declare -a domains # Declare indexed array to hold domain references
|
||||||
|
domains=( d0 d1 )
|
||||||
|
```
|
||||||
|
Ordered collections for managing multiple domains.
|
||||||
|
|
||||||
|
### 5. **Array References (Namerefs)**
|
||||||
|
```bash
|
||||||
|
declare -n domain="$item" # Create reference to associative array
|
||||||
|
echo "${domain[domain]}" # Access array elements through reference
|
||||||
|
```
|
||||||
|
Dynamic array referencing for flexible data access.
|
||||||
|
|
||||||
|
### 6. **Functions**
|
||||||
|
```bash
|
||||||
|
get_current_ip() {
|
||||||
|
curl -s https://ifconfig.me/ip
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dns_record() {
|
||||||
|
local zone_id="$1"
|
||||||
|
local data="$2"
|
||||||
|
# Function implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Modular code organization with local variables and parameters.
|
||||||
|
|
||||||
|
### 7. **Advanced Conditional Statements**
|
||||||
|
```bash
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo "⚠️ DRY RUN MODE ENABLED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${existing_a_records[$full_name]}" ]]; then
|
||||||
|
# Record exists logic
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
String comparison, variable existence checks, and complex conditions.
|
||||||
|
|
||||||
|
### 8. **Loop Constructs**
|
||||||
|
```bash
|
||||||
|
for item in "${domains[@]}"; do
|
||||||
|
# Process each domain
|
||||||
|
done
|
||||||
|
|
||||||
|
for subdomain in "${managed_subdomains[@]}"; do
|
||||||
|
# Process each subdomain
|
||||||
|
done
|
||||||
|
```
|
||||||
|
Iteration over arrays with proper quoting.
|
||||||
|
|
||||||
|
### 9. **String Manipulation and Parsing**
|
||||||
|
```bash
|
||||||
|
IFS=',' read -ra protected_records <<< "${domain[protected_records]}"
|
||||||
|
IFS=',' read -ra managed_subdomains <<< "${domain[subdomains]}"
|
||||||
|
```
|
||||||
|
Field splitting and array population from comma-separated strings.
|
||||||
|
|
||||||
|
### 10. **Command and Process Substitution**
|
||||||
|
```bash
|
||||||
|
current_ip=$(get_current_ip) # Command substitution
|
||||||
|
|
||||||
|
done < <(echo "$response" | jq -c '.result[] | select(.type == "A")') # Process substitution
|
||||||
|
```
|
||||||
|
Capturing command output and feeding output to loops.
|
||||||
|
|
||||||
|
### 11. **JSON Processing with jq**
|
||||||
|
```bash
|
||||||
|
if echo "$response" | jq -e '.success == false' > /dev/null 2>&1; then
|
||||||
|
echo "Error: Failed to get DNS records"
|
||||||
|
fi
|
||||||
|
|
||||||
|
create_data=$(jq -n \
|
||||||
|
--arg name "$full_name" \
|
||||||
|
--arg ip "$current_ip" \
|
||||||
|
'{
|
||||||
|
"type": "A",
|
||||||
|
"name": $name,
|
||||||
|
"content": $ip,
|
||||||
|
"ttl": 1,
|
||||||
|
"proxied": true
|
||||||
|
}')
|
||||||
|
```
|
||||||
|
Parsing JSON responses and constructing JSON payloads.
|
||||||
|
|
||||||
|
### 12. **Error Handling and Exit Codes**
|
||||||
|
```bash
|
||||||
|
if [ -z "$current_ip" ]; then
|
||||||
|
echo "Error: Could not retrieve current IP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
Input validation and controlled script termination.
|
||||||
|
|
||||||
|
### 13. **Arithmetic Operations**
|
||||||
|
```bash
|
||||||
|
((records_created++))
|
||||||
|
((records_updated++))
|
||||||
|
((records_skipped++))
|
||||||
|
((records_deleted++))
|
||||||
|
```
|
||||||
|
Counter variables for operation statistics.
|
||||||
|
|
||||||
|
### 14. **API Integration with curl**
|
||||||
|
```bash
|
||||||
|
curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \
|
||||||
|
-X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-d "$data"
|
||||||
|
```
|
||||||
|
REST API calls with proper headers and data payloads.
|
||||||
|
|
||||||
|
### 15. **Dynamic Variable Management**
|
||||||
|
```bash
|
||||||
|
unset existing_a_records # Clean up associative arrays
|
||||||
|
unset domain # Clean up nameref
|
||||||
|
```
|
||||||
|
Memory management and variable cleanup.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Configure Domains**: Edit the domain arrays at the top of the script
|
||||||
|
2. **Set API Token**: Update the `TOKEN` variable with your Cloudflare API token
|
||||||
|
3. **Dry Run First**: Test with `DRY_RUN=true` to see what would change
|
||||||
|
4. **Run Updates**: Set `DRY_RUN=false` to apply actual changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry run (safe testing)
|
||||||
|
./update_domain.sh
|
||||||
|
|
||||||
|
# Apply changes
|
||||||
|
# Edit script: DRY_RUN=false
|
||||||
|
./update_domain.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Domain Configuration Structure
|
||||||
|
Each domain is configured as an associative array with:
|
||||||
|
- `domain`: The root domain name
|
||||||
|
- `zone_id`: Cloudflare zone identifier
|
||||||
|
- `subdomains`: Comma-separated list of subdomains to manage
|
||||||
|
- `protected_records`: Records that should never be deleted
|
||||||
|
|
||||||
|
### Script Parameters
|
||||||
|
- `TOKEN`: Cloudflare API bearer token
|
||||||
|
- `AUTO_DELETE_ORPHANS`: Whether to remove unmanaged records
|
||||||
|
- `DRY_RUN`: Test mode without making actual changes
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Bash 4.0+
|
||||||
|
- curl
|
||||||
|
- jq
|
||||||
|
- Valid Cloudflare API token with DNS edit permissions
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Store API tokens securely (consider environment variables)
|
||||||
|
- Use dry run mode when testing
|
||||||
|
- Review protected records configuration carefully
|
||||||
|
- Monitor script output for any API errors
|
||||||
|
|
||||||
|
## Output Example
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ DRY RUN MODE ENABLED - No actual changes will be made
|
||||||
|
=========================================
|
||||||
|
Current Public IP: 203.0.113.1
|
||||||
|
=========================================
|
||||||
|
Processing domain: my-dev.pro
|
||||||
|
Zone ID: 5c9a19af6aeababb78b294844290a7d2
|
||||||
|
Managed subdomains: @ git
|
||||||
|
----------------------------------------
|
||||||
|
Existing A records:
|
||||||
|
- my-dev.pro → 203.0.113.1 (ID: abc123)
|
||||||
|
- git.my-dev.pro → 203.0.113.1 (ID: def456)
|
||||||
|
----------------------------------------
|
||||||
|
Checking: my-dev.pro
|
||||||
|
✓ SKIPPED: IP unchanged (203.0.113.1 = 203.0.113.1)
|
||||||
|
Checking: git.my-dev.pro
|
||||||
|
✓ SKIPPED: IP unchanged (203.0.113.1 = 203.0.113.1)
|
||||||
|
=========================================
|
||||||
|
SUMMARY for my-dev.pro:
|
||||||
|
✓ Created: 0
|
||||||
|
✓ Updated: 0
|
||||||
|
✓ Skipped (IP unchanged): 2
|
||||||
|
✓ Deleted: 0
|
||||||
|
=========================================
|
||||||
|
⚠️ DRY RUN COMPLETED - No actual changes were made
|
||||||
|
Set DRY_RUN=false to apply changes
|
||||||
|
```
|
||||||
Executable
+299
@@ -0,0 +1,299 @@
|
|||||||
|
#!bin/bash
|
||||||
|
|
||||||
|
#=================#
|
||||||
|
# params
|
||||||
|
#=================#
|
||||||
|
TOKEN="cfut_wgrWeIbAuyqekbEd1EqCQFSR6NNmE0gzJdReSZoce3352365"
|
||||||
|
AUTO_DELETE_ORPHANS=true # Set to true to auto-delete orphaned records
|
||||||
|
DRY_RUN=false # Set to false to actually create records
|
||||||
|
|
||||||
|
|
||||||
|
#################
|
||||||
|
# Domain list
|
||||||
|
#################
|
||||||
|
declare -a domains # declare main domain associative array to include all domains in one array
|
||||||
|
declare -A d0 d1 # declare new array for each domain
|
||||||
|
|
||||||
|
d0=(
|
||||||
|
["domain"]=""
|
||||||
|
["zone_id"]=""
|
||||||
|
["subdomains"]="@,git"
|
||||||
|
["protected_records"]=""
|
||||||
|
)
|
||||||
|
|
||||||
|
domains=( d0 d1 )
|
||||||
|
|
||||||
|
#*****************#
|
||||||
|
# DNS FUNCTIONS
|
||||||
|
#*****************#
|
||||||
|
get_current_ip() {
|
||||||
|
curl -s https://ifconfig.me/ip
|
||||||
|
}
|
||||||
|
|
||||||
|
list_dns_records() {
|
||||||
|
local zone_id="$1"
|
||||||
|
curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dns_record() {
|
||||||
|
local zone_id="$1"
|
||||||
|
local data="$2"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo " [DRY RUN] Would create record with: $data"
|
||||||
|
echo '{"success":true,"dry_run":true}'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" \
|
||||||
|
-X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-d "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_dns_record() {
|
||||||
|
local zone_id="$1"
|
||||||
|
local dns_record_id="$2"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo " [DRY RUN] Would delete record ID: $dns_record_id"
|
||||||
|
echo '{"success":true,"dry_run":true}'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$dns_record_id" \
|
||||||
|
-X DELETE \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_dns_record() {
|
||||||
|
local zone_id="$1"
|
||||||
|
local dns_record_id="$2"
|
||||||
|
local body="$3"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo " [DRY RUN] Would update record ID: $dns_record_id with: $body"
|
||||||
|
echo '{"success":true,"dry_run":true}'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$dns_record_id" \
|
||||||
|
-X PATCH \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-d "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
#*****************#
|
||||||
|
# HELPER FUNCTIONS
|
||||||
|
#*****************#
|
||||||
|
compare_ips() {
|
||||||
|
local ip1="$1"
|
||||||
|
local ip2="$2"
|
||||||
|
|
||||||
|
if [[ "$ip1" == "$ip2" ]]; then
|
||||||
|
return 0 # IPs are the same
|
||||||
|
else
|
||||||
|
return 1 # IPs are different
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#**************#
|
||||||
|
# MAIN OPERATIONS
|
||||||
|
#**************#
|
||||||
|
|
||||||
|
# Display mode
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo "⚠️ DRY RUN MODE ENABLED - No actual changes will be made"
|
||||||
|
echo "========================================="
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current public IP
|
||||||
|
current_ip=$(get_current_ip)
|
||||||
|
|
||||||
|
if [ -z "$current_ip" ]; then
|
||||||
|
echo "Error: Could not retrieve current IP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Current Public IP: $current_ip"
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# Process each domain
|
||||||
|
for item in "${domains[@]}"; do
|
||||||
|
unset domain
|
||||||
|
declare -n domain="$item"
|
||||||
|
|
||||||
|
echo "Processing domain: ${domain[domain]}"
|
||||||
|
echo "Zone ID: ${domain[zone_id]}"
|
||||||
|
|
||||||
|
# Parse protected records
|
||||||
|
IFS=',' read -ra protected_records <<< "${domain[protected_records]}"
|
||||||
|
|
||||||
|
# Parse managed subdomains
|
||||||
|
IFS=',' read -ra managed_subdomains <<< "${domain[subdomains]}"
|
||||||
|
echo "Managed subdomains: ${managed_subdomains[@]}"
|
||||||
|
|
||||||
|
# Get existing DNS records
|
||||||
|
response=$(list_dns_records "${domain[zone_id]}")
|
||||||
|
|
||||||
|
# Check for API errors
|
||||||
|
if echo "$response" | jq -e '.success == false' > /dev/null 2>&1; then
|
||||||
|
echo "Error: Failed to get DNS records"
|
||||||
|
echo "$response" | jq '.errors'
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Create associative array for existing A records
|
||||||
|
declare -A existing_a_records
|
||||||
|
|
||||||
|
# Build map of existing A records
|
||||||
|
echo "Existing A records:"
|
||||||
|
while IFS= read -r record; do
|
||||||
|
if [[ -n "$record" ]]; then
|
||||||
|
record_id=$(echo "$record" | jq -r '.id')
|
||||||
|
record_name=$(echo "$record" | jq -r '.name')
|
||||||
|
record_content=$(echo "$record" | jq -r '.content')
|
||||||
|
|
||||||
|
existing_a_records["$record_name"]="$record_id|$record_content"
|
||||||
|
echo " - $record_name → $record_content (ID: $record_id)"
|
||||||
|
fi
|
||||||
|
done < <(echo "$response" | jq -c '.result[] | select(.type == "A")')
|
||||||
|
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Statistics counters
|
||||||
|
records_created=0
|
||||||
|
records_updated=0
|
||||||
|
records_skipped=0
|
||||||
|
records_deleted=0
|
||||||
|
|
||||||
|
# Process each managed subdomain (create/update)
|
||||||
|
for subdomain in "${managed_subdomains[@]}"; do
|
||||||
|
# Build full domain name
|
||||||
|
if [[ "$subdomain" == "@" ]] || [[ -z "$subdomain" ]]; then
|
||||||
|
full_name="${domain[domain]}"
|
||||||
|
else
|
||||||
|
full_name="${subdomain}.${domain[domain]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking: $full_name"
|
||||||
|
|
||||||
|
# Check if record exists
|
||||||
|
if [[ -n "${existing_a_records[$full_name]}" ]]; then
|
||||||
|
IFS='|' read -r record_id existing_ip <<< "${existing_a_records[$full_name]}"
|
||||||
|
|
||||||
|
# Compare IPs
|
||||||
|
if compare_ips "$existing_ip" "$current_ip"; then
|
||||||
|
echo " ✓ SKIPPED: IP unchanged ($existing_ip = $current_ip)"
|
||||||
|
((records_skipped++))
|
||||||
|
else
|
||||||
|
echo " ⚠ NEEDS UPDATE: IP changed ($existing_ip → $current_ip)"
|
||||||
|
|
||||||
|
# Update existing record with new IP
|
||||||
|
update_body=$(jq -n --arg ip "$current_ip" '{"content": $ip}')
|
||||||
|
update_response=$(update_dns_record "${domain[zone_id]}" "$record_id" "$update_body")
|
||||||
|
|
||||||
|
if echo "$update_response" | jq -e '.success == true' > /dev/null 2>&1; then
|
||||||
|
echo " ✓ SUCCESS: Updated $full_name to $current_ip"
|
||||||
|
((records_updated++))
|
||||||
|
else
|
||||||
|
echo " ✗ FAILED: Could not update $full_name"
|
||||||
|
echo "$update_response" | jq '.errors'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mark as processed (remove from map for deletion check)
|
||||||
|
unset existing_a_records["$full_name"]
|
||||||
|
else
|
||||||
|
echo " ➕ CREATING: Record does not exist"
|
||||||
|
|
||||||
|
# Create new record
|
||||||
|
create_data=$(jq -n \
|
||||||
|
--arg name "$full_name" \
|
||||||
|
--arg ip "$current_ip" \
|
||||||
|
'{
|
||||||
|
"type": "A",
|
||||||
|
"name": $name,
|
||||||
|
"content": $ip,
|
||||||
|
"ttl": 1,
|
||||||
|
"proxied": true
|
||||||
|
}')
|
||||||
|
|
||||||
|
create_response=$(create_dns_record "${domain[zone_id]}" "$create_data")
|
||||||
|
|
||||||
|
if echo "$create_response" | jq -e '.success == true' > /dev/null 2>&1; then
|
||||||
|
echo " ✓ SUCCESS: Created $full_name -> $current_ip"
|
||||||
|
((records_created++))
|
||||||
|
else
|
||||||
|
echo " ✗ FAILED: Could not create $full_name"
|
||||||
|
echo "$create_response" | jq '.errors'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Delete orphaned records if auto-delete is enabled
|
||||||
|
if [[ "$AUTO_DELETE_ORPHANS" == true ]] && [[ ${#existing_a_records[@]} -gt 0 ]]; then
|
||||||
|
echo "Checking for orphaned records to delete..."
|
||||||
|
|
||||||
|
for record_name in "${!existing_a_records[@]}"; do
|
||||||
|
# Check if record is protected
|
||||||
|
is_protected=false
|
||||||
|
for protected in "${protected_records[@]}"; do
|
||||||
|
if [[ "$record_name" == "$protected" ]]; then
|
||||||
|
is_protected=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$is_protected" == true ]]; then
|
||||||
|
echo " ⚠ PROTECTED: $record_name (skipped from deletion)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS='|' read -r record_id existing_ip <<< "${existing_a_records[$record_name]}"
|
||||||
|
echo " 🗑 ORPHANED: $record_name -> $existing_ip"
|
||||||
|
|
||||||
|
delete_response=$(delete_dns_record "${domain[zone_id]}" "$record_id")
|
||||||
|
|
||||||
|
if echo "$delete_response" | jq -e '.success == true' > /dev/null 2>&1; then
|
||||||
|
echo " ✓ DELETED: $record_name"
|
||||||
|
((records_deleted++))
|
||||||
|
else
|
||||||
|
echo " ✗ FAILED: Could not delete $record_name"
|
||||||
|
echo "$delete_response" | jq '.errors'
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
elif [[ ${#existing_a_records[@]} -gt 0 ]]; then
|
||||||
|
echo "Orphaned records found (auto-delete disabled):"
|
||||||
|
for record_name in "${!existing_a_records[@]}"; do
|
||||||
|
IFS='|' read -r record_id existing_ip <<< "${existing_a_records[$record_name]}"
|
||||||
|
echo " - $record_name -> $existing_ip (ID: $record_id)"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo "========================================="
|
||||||
|
echo "SUMMARY for ${domain[domain]}:"
|
||||||
|
echo " ✓ Created: $records_created"
|
||||||
|
echo " ✓ Updated: $records_updated"
|
||||||
|
echo " ✓ Skipped (IP unchanged): $records_skipped"
|
||||||
|
echo " ✓ Deleted: $records_deleted"
|
||||||
|
echo "========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean up associative array
|
||||||
|
unset existing_a_records
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo "⚠️ DRY RUN COMPLETED - No actual changes were made"
|
||||||
|
echo "Set DRY_RUN=false to apply changes"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user