#!/usr/bin/env python3 # script to help manage dns through cloudflare # https://api.cloudflare.com # # this script was made to be compatible with the previous cloudflare_dns.pl script, argument names and functions kept the same # # jason jorgensen # 2018-09-04 - initial version created from cloudflare_dns.pl import sys import json import argparse import CloudFlare debug = False cf = CloudFlare.CloudFlare() zones = cf.zones.get() def print_usage(): print(''' Usage: {0} (action arguments space delimited) Examples: {0} zones #list available zones {0} list {0} list domain=example.com {0} list domain=example.com output=bind # output all the existing records in a bind-ish format {0} list domain=example.com output=json # output all the existing records in json format {0} add domain=example.com type=A sub=testrecord value=127.0.0.1 mode=1 # create a new 'A' record where cloudflare will proxy {0} add domain=example.com type=MX sub=testmail value=127.0.0.1 priority=10 mode=0 # create a new 'MX' record where cloudflare will not proxy {0} delete domain=example.com type=A sub=testrecord # delete a record with a sub {0} delete domain=example.com id=634354764 # delete a single record with id Note: raw output (default) is terse json format json output is human readable json format Actions: list : domain=example.com output=[bind, csv, json, json_many] #json is one large object, json_many is an object per record add : domain=example.com type=[A, CNAME, MX, NS, TXT AAAA, SRV, LOC, SPF, CERT, DNSKEY, DS, NAPTR, SMIMEA, SSHFP, TLSA, URI] sub=www or mail priority=10 value=271.23.51.83 or example.com delete : domain=example.com sub=www or mail id=35907682 or 35924682 '''.format(sys.argv[0])) def get_zone_id(name): for zone in zones: if zone['name'] == name: return zone['id'] raise Exception('no zone by that name') def validate_zone_exists(name): for zone in zones: if zone['name'] == name: return zone['name'] raise Exception('no zone by that name') if __name__ == '__main__': # step through arguments that are not an action(argv[1]) output = None domains = None ttype = None sub = None iid = None value = None priority = None mode = None for argvalue in sys.argv[2:]: if debug: print('argument/value pair: {}'.format(argvalue)) arg, argvalue = argvalue.split('=', 1) if debug: print('argument/value split: {} {}'.format(arg, argvalue)) if 'output' in arg: output = argvalue if 'domain' in arg: domains = argvalue.split(",") if 'type' in arg: ttype = argvalue if 'sub' in arg: sub = argvalue if 'id' in arg: iid = argvalue if 'value' in arg: value = argvalue if 'priority' in arg: priority = argvalue if 'mode' in arg: mode = bool(int(argvalue)) if debug: print('output: {}'.format(output)) print('domain: {}'.format(domains)) print('type: {}'.format(ttype)) print('sub: {}'.format(sub)) print('id: {}'.format(iid)) print('value: {}'.format(value)) print('priority: {}'.format(priority)) print('mode: {}'.format(mode)) if len(sys.argv) > 1: action = sys.argv[1] if len(sys.argv) == 1: print_usage() sys.exit(1) elif action == 'zones': for zone in zones: zone_id = zone['id'] zone_name = zone['name'] print(zone_id, zone_name) elif action == 'add': if not domains[0]: print("Please specify a domain as argument, 'domain=example.com") if not ttype: print("Please specify a record type as argument, 'type=[A, CNAME, MX, NS, TXT, SRV]'") if not sub: print("Please specify a subdomain name as argument, 'sub=www' or 'sub=mail.office' or 'sub=ar_fre.photos'") if not value: print("Please specify a value for your record type as argument, 'value=1.2.3.4' or 'value=fully.qualified.domain.name'") if not mode: mode = False if domains[0] and ttype and sub and value: zone_name = validate_zone_exists(domains[0]) zone_id = get_zone_id(domains[0]) record = {'name':sub, 'type':ttype, 'content':value, 'proxied':mode} if priority: record['priority'] = priority if debug: print('adding dns record to {}({}): {}'.format(zone_name, zone_id, record)) result = cf.zones.dns_records.post(zone_id, data=record) print(result) else: print('Missing information, not adding any records') sys.exit(1) elif action == 'list': if not domains: print("Please specify a domain as argument, 'domain=example.com") if not output: output = 'bind' for domain in domains: zone_name = validate_zone_exists(domain) zone_id = get_zone_id(domain) # $ORIGIN . # @ 3600 IN SOA example.com. root.example.com. ( # 2028774503 ; serial # 7200 ; refresh # 3600 ; retry # 86400 ; expire # 3600) ; minimum # search.dev.example.com. 1 IN NS ns3.prod.office.example.com. ## print the BIND zone header only dns_records = cf.zones.dns_records.export.get(zone_id) if output == 'bind': for dns_record in dns_records.splitlines(): if len(dns_record) == 0 or dns_record[0] == ';': # blank line or comment line are skipped - to make example easy to see continue # first line starting with an alphanumeric character is after the header and we are done if dns_record[0].isalpha(): break print(dns_record) ## print zone records dns_records = [1] dns_records_page = 1 json_one = [] while len(dns_records) > 0: dns_records = cf.zones.dns_records.get(zone_id, params={'per_page':100, 'page':dns_records_page}) for dns_record in dns_records: # {'type': 'A' # 'proxied': False # 'name': 'admin.office.example.com' # 'meta': {'auto_added': False # 'managed_by_apps': False # 'managed_by_argo_tunnel': False} # 'zone_name': 'example.com' # 'proxiable': False # 'ttl': 1 # 'id': 'ae25a0a9575dada06715a5c7fbaba3bd' # 'created_on': '2018-05-03T16:17:52.456085Z' # 'locked': False # 'content': '172.28.0.191' # 'modified_on': '2018-05-03T16:17:52.456085Z' # 'zone_id': '124dccdf126dcc022950be0ded4343bf'} if output == 'bind': # search.dev.example.com. 1 IN NS ns3.prod.office.example.com. print('{}.\t{}\tIN\t{}\t{};\tid = {}\tcached = {}\t modified = {}'.format(dns_record['name'], dns_record['ttl'], dns_record['type'], dns_record['content'], dns_record['id'], dns_record['proxiable'], dns_record['modified_on'])) if output == 'csv': print('"{}","{}","{}","{}","{}"'.format(dns_record['name'], dns_record['type'], dns_record['content'], dns_record['id'], dns_record['proxiable'])) if output == 'json_many': print(json.dumps(dns_record, sort_keys=True, indent=2)) if output == 'json': json_one.append(dns_record) dns_records_page += 1 if len(json_one) > 0: print(json.dumps(json_one, sort_keys=True, indent=2)) elif action == 'delete': if not domains[0]: print("Please specify a domain as argument, 'domain=example.com") if (not iid) and (not ttype or not sub): print("Please specify the id or type and sub of the dns record you want to delete, 'id=206c6e676507c92d52c3f39704587b8' 'type=A sub=testrecord'") zone_name = validate_zone_exists(domains[0]) zone_id = get_zone_id(domains[0]) ## the api only deletes based on record id, no longer works via sub + type. so we have to do that lookup ourselves if domains[0] and ttype and sub: if zone_name not in sub: name = '{}.{}'.format(sub, zone_name) else: name = sub params = {'name': name, 'type': ttype} if debug: print('delete: type and sub supplied, looking up id of record via api for "{}"'.format(params)) records = cf.zones.dns_records.get(zone_id, params=params) if debug: print('delete: type and sub search result: {}'.format(records)) if len(records) == 0: print('Could not find any matching dns records for the following parameters: {}'.format(params)) sys.exit(1) if len(records) > 1: print('This matches multiple records and is not specific enough! Not deleting') for record in records: print(record) sys.exit(1) iid = records[0]['id'] ## delete record based on id if domains[0] and iid: record = cf.zones.dns_records.get(zone_id, iid) if debug: print(record) print('deleting dns record id from {}({}): {} {} {}'.format(zone_name, zone_id, iid, record['name'], record['content'])) result = cf.zones.dns_records.delete(zone_id, iid) print(result) else: print('Missing information, not deleting any records') sys.exit(1) else: print('unknown action requested: {}'.format(action)) print_usage() sys.exit(1)