diff --git a/bulk_dns_update.py b/bulk_dns_update.py new file mode 100644 index 0000000..d4d54ea --- /dev/null +++ b/bulk_dns_update.py @@ -0,0 +1,146 @@ +#!/usr/bin/python3 +''' +Cloudflare bulk DNS A record updater. + +Designed to easily update all A records in cloudflare +from one IP to another. Usually after a new IP is +leased by ISP. + +BEFORE RUNNING replace API_KEY with a key that holds +permission: DNS:Edit for one or more zones. +''' + +import re +import requests + +API_KEY = '' + + +def setAPIKey(apiKey): + global API_KEY + API_KEY = apiKey + + +def cloudflareRequest(url): + headers = { + 'Authorization': 'Bearer {}'.format(API_KEY), + 'Content-Type': 'application/json' + } + + r = requests.get(url, headers=headers) + return r.json() + + +def cloudflareUpdateRequest(url, data): + headers = { + 'Authorization': 'Bearer {}'.format(API_KEY), + 'Content-Type': 'application/json' + } + + r = requests.patch(url, headers=headers, json=data) + return r.json() + + +def getZoneInfo(zone): + return { + 'name': zone['name'], + 'id': zone['id'] + } + + +def getRecordInfo(record): + return { + 'content': record['content'], + 'name': record['name'], + 'id': record['id'], + 'ttl': record['ttl'], + 'proxied': record['proxied'] + } + + +def getZones(): + url = 'https://api.cloudflare.com/client/v4/zones' + data = cloudflareRequest(url) + + if data['success'] is False: + print('Request to cloudflare was unsuccessful, error:') + print(data['errors']) + raise Exception('Unexpected Cloudflare error! Check logs.') + + if data['result'] is None or len(data['result']) < 1: + # TODO + print('no zones!') + + zones = list(map(lambda zone: getZoneInfo(zone), data['result'])) + return zones + + +def getRecordsForZone(zoneId): + url = 'https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A'.format(zoneId) + data = cloudflareRequest(url) + + if data['success'] is False: + print('Request from cloudflare was unsuccessful, error:') + print(data['errors']) + raise Exception('Unexpected Cloudflare error! Check logs.') + + if data['result'] is None or len(data['result']) < 1: + # TODO + print('no records!') + + records = list(map(lambda record: getRecordInfo(record), data['result'])) + return records + + +def getDDNSAddresszoneId(ddnsZone): + records = getRecordsForZone(ddnsZone) + + for record in records: + if not re.match(r"^addr\.", record['name']): + continue + return record + + raise Exception('No ddns record found for zone: {}'.format(DDNS_ZONE)) + + +def updateRecord(zoneId, recordId, name, newIP, ttl, proxied): + url = 'https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}'.format(zoneId, recordId) + data = { + 'type': 'A', + 'name': name, + 'content': newIP, + 'ttl': ttl, + 'proxied': proxied + } + + response = cloudflareUpdateRequest(url, data) + print('\tRecord updated: {}'.format('✅' if response['success'] is True else '❌')) + + +def getMatchingRecordsForZone(zoneId, oldIP): + records = getRecordsForZone(zoneId) + return list(filter(lambda record: record['content'] == oldIP, records)) + + +def updateZone(zone, oldIP, newIP): + print('Updating records for {}'.format(zone['name'])) + records = getMatchingRecordsForZone(zone['id'], oldIP) + if len(records) < 1: + print('No matching records for {}\n'.format(zone['name'])) + return + + for record in records: + print('\tRecord {}: {} -> {}'.format(record['name'], record['content'], newIP)) + updateRecord(zone['id'], record['id'], record['name'], newIP, record['ttl'], record['proxied']) + +def updateAllZones(oldIP, newIP): + zones = getZones() + + for zone in zones: + updateZone(zone, oldIP, newIP) + +if __name__ == '__main__': + oldIP = input('Old IP address: ') + newIP = input('New IP address: ') + + updateAllZones(oldIP, newIP) diff --git a/main.py b/main.py new file mode 100644 index 0000000..9a55304 --- /dev/null +++ b/main.py @@ -0,0 +1,48 @@ +import os +import requests +from bulk_dns_update import updateAllZones, setAPIKey, getDDNSAddresszoneId +from dotenv import load_dotenv + +load_dotenv() + +currentIP = None +DDNS_ZONE = os.getenv('DDNS_ZONE') + + +def publicAddress(): + global currentIP + print('Getting public IP from ifconfg.me...') + + r = requests.get('https://ifconfig.me') + currentIP = r.text + print('Public IP: {}'.format(currentIP)) + + +def cloudflareDDNS(): + print('Checking IP recorded in Cloudflare...') + ddnsRecord = getDDNSAddresszoneId(DDNS_ZONE) + recordedIP = ddnsRecord['content'] + print('Found ddns recorded IP: {}'.format(recordedIP)) + + if currentIP != recordedIP: + print('Public IP has changed, updating all A records.') + updateAllZones(recordedIP, currentIP) + else: + print('is same, exiting') + + +def main(): + apiKey = os.getenv('API_KEY') + if apiKey is None: + raise Exception('In .env file or environment set Cloudflare variable: API_KEY') + if DDNS_ZONE is None: + raise Exception('In .env file or environment; set Cloudflare zone where addr. points to current IP.') + + setAPIKey(apiKey) + + publicAddress() + cloudflareDDNS() + + +if __name__ == '__main__': + main()