-
-
Save jikamens/3d8f018c47e4c4f0dd87b8c31bc57076 to your computer and use it in GitHub Desktop.
| #!/usr/bin/env python3 | |
| """ | |
| namecheap-dns.py - Export/import DNS records from/to Namecheap | |
| This script can export DNS records from a domain in Namecheap to a YAML file or | |
| import records from a YAML file in the same format into Namecheap. I use this | |
| script to maintain my Namecheap DNS records in a source repository with change | |
| history, i.e., "configuration as code" for my Namecheap DNS records. | |
| Beware! The Namecheap API doesn't allow creating, modifying, or deleting | |
| individual records. The only write operation supported by the API is replacing | |
| all the records for the domain. Therefore, the expected way to use this script | |
| with a domain that has records in it predating your use of the script is to | |
| export all of the current records, modify the export file to reflect any | |
| changes you want to make, and then import the modified file, possibly first | |
| running the import with --dryrun to make sure it's going to do what you expect. | |
| To use the script you need to enable the Namecheap API on your account and | |
| whitelist the public IPv4 address of the host you're running the script on. See | |
| https://www.namecheap.com/support/api/intro/ for details. | |
| You need to have a config file, by default named namecheap-dns-config.yml in | |
| the current directory though you can specify a different file on the command | |
| line, whose contents look like this: | |
| ApiUser: [Namecheap username] | |
| UserName: [Namecheap username] | |
| ApiKey: [Namecheap API keyi] | |
| ClientIP: [public IPv4 address of the host you're running the script on] | |
| The YAML file containing the records looks like this: | |
| - Address: 127.0.0.1 | |
| HostName: localhost | |
| RecordType: A | |
| TTL: '180' | |
| - Address: 192.168.0.1 | |
| HostName: router | |
| RecordType: A | |
| - Address: email.my.domain | |
| MXPref: 10 | |
| HostName: '@' | |
| RecordType: MX | |
| The order of records or of fields within individual records doesn't matter. | |
| Of note: there's no way through this API to create records that show up on the | |
| Namecheap Advanced DNS page as "dynamic DNS records," so if you have such a | |
| record created through that page and then you export and import your records, | |
| it will be converted into a regular DNS record. This doesn't seem to matter, | |
| though, because the dynamic DNS update API will still work on it. :shrug: | |
| Copyright 2023 Jonathan Kamens <[email protected]> | |
| This program is free software: you can redistribute it and/or modify it under | |
| the terms of the GNU General Public License as published by the Free Software | |
| Foundation, either version 3 of the License, or (at your option) any later | |
| version. | |
| This program is distributed in the hope that it will be useful, but WITHOUT ANY | |
| WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A | |
| PARTICULAR PURPOSE. See the GNU General Public License at | |
| <https://www.gnu.org/licenses/> for more details. | |
| """ | |
| import argparse | |
| from itertools import count | |
| import json | |
| from lxml import etree | |
| import requests | |
| import sys | |
| import yaml | |
| namecheap_api_url = 'https://api.namecheap.com/xml.response' | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description='Export or import Namecheap ' | |
| 'DNS records') | |
| parser.add_argument('--config-file', action='store', | |
| default='namecheap-dns-config.yml', | |
| help='Config file (default namecheap-dns-config.yml)') | |
| subparsers = parser.add_subparsers(title='subcommands', required=True) | |
| import_parser = subparsers.add_parser( | |
| 'import', description='Import records into Namecheap') | |
| import_parser.set_defaults(command=do_import) | |
| import_parser.add_argument('--dryrun', action='store_true', default=False, | |
| help='Say what would be done without doing it') | |
| import_parser.add_argument('--input-file', type=argparse.FileType('r'), | |
| default=sys.stdin, help='File to read records ' | |
| 'from (default stdin)') | |
| import_parser.add_argument('domain') | |
| export_parser = subparsers.add_parser( | |
| 'export', description='Export records from Namecheap') | |
| export_parser.set_defaults(command=do_export) | |
| export_parser.add_argument('--output-file', type=argparse.FileType('w'), | |
| default=sys.stdout, help='File to write ' | |
| 'records to (default stdout)') | |
| export_parser.add_argument('domain') | |
| args = parser.parse_args() | |
| return args | |
| def make_namecheap_request(config, data): | |
| request = data.copy() | |
| request.update({ | |
| 'ApiUser': config['ApiUser'], | |
| 'UserName': config['UserName'], | |
| 'ApiKey': config['ApiKey'], | |
| 'ClientIP': config['ClientIP'], | |
| }) | |
| response = requests.post(namecheap_api_url, request) | |
| response.raise_for_status() | |
| response_xml = etree.XML(response.content) | |
| if response_xml.get('Status') != 'OK': | |
| raise Exception('Bad response: {}'.format(response.content)) | |
| return response_xml | |
| def get_records(config, sld, tld): | |
| response = make_namecheap_request(config, { | |
| 'Command': 'namecheap.domains.dns.getHosts', | |
| 'SLD': sld, | |
| 'TLD': tld}) | |
| host_elements = response.xpath( | |
| '/x:ApiResponse/x:CommandResponse/x:DomainDNSGetHostsResult/x:host', | |
| namespaces={'x': 'http://api.namecheap.com/xml.response'}) | |
| records = [dict(h.attrib) for h in host_elements] | |
| for record in records: | |
| record.pop('AssociatedAppTitle', None) | |
| record.pop('FriendlyName', None) | |
| record.pop('HostId', None) | |
| record['HostName'] = record.pop('Name') | |
| record.pop('IsActive', None) | |
| record.pop('IsDDNSEnabled', None) | |
| if record['Type'] != 'MX': | |
| record.pop('MXPref', None) | |
| record['RecordType'] = record.pop('Type') | |
| if record['TTL'] == '1800': | |
| record.pop('TTL') | |
| return records | |
| def do_import(args, config): | |
| current = {dict_hash(r): r | |
| for r in get_records(config, args.sld, args.tld)} | |
| new = {dict_hash(r): r | |
| for r in yaml.safe_load(args.input_file)} | |
| changed = False | |
| for r in current.keys(): | |
| if r not in new: | |
| print(f'Removing {current[r]}') | |
| changed = True | |
| for r in new.keys(): | |
| if r not in current: | |
| print(f'Adding {new[r]}') | |
| changed = True | |
| if not changed: | |
| return | |
| data = { | |
| 'Command': 'namecheap.domains.dns.setHosts', | |
| 'SLD': args.sld, | |
| 'TLD': args.tld, | |
| } | |
| for num, record in zip(count(1), new.values()): | |
| for key, value in record.items(): | |
| data[f'{key}{num}'] = value | |
| if not args.dryrun: | |
| make_namecheap_request(config, data) | |
| def do_export(args, config): | |
| records = get_records(config, args.sld, args.tld) | |
| yaml.dump(sorted(records, key=dict_hash), args.output_file) | |
| def dict_hash(d): | |
| d = d.copy() | |
| name = d.pop('HostName') | |
| type_ = d.pop('RecordType') | |
| return (type_, name, json.dumps(d, sort_keys=True)) | |
| def main(): | |
| args = parse_args() | |
| (args.sld, args.tld) = args.domain.split('.', 1) | |
| config = yaml.safe_load(open(args.config_file)) | |
| args.command(args, config) | |
| if __name__ == '__main__': | |
| main() |
Thanks for posting this script! Just in case this helps anyone in future -- I had to make a tiny tweak on line 187 to change it to
args.domain.split('.', 1)to handle domains like.co.ukwhich have 2.'s!
Thanks, fixed!
instead of yaml file maybe a better option could be the bind format
Note, the instructions (line 23) says to a config file, by default named namecheap-dns-config.json. However, the code itself it looking for a .yml file, not a .json file.
instead of yaml file maybe a better option could be the bind format
🤷 The BIND format is harder to generate and parse, and exporting to BIND format wasn't the point of writing the script, so I didn't bother.
Note, the instructions (line 23) says to a config file, by default named
namecheap-dns-config.json. However, the code itself it looking for a.ymlfile, not a.jsonfile.
Thanks, fixed.
Very nice script and very useful for backing up Namecheap DNS records.
I'd love to see this converted into a full Python module with programmatic access and standalone access via command line.
Thanks for posting this script! Just in case this helps anyone in future -- I had to make a tiny tweak on line 187 to change it to
args.domain.split('.', 1)to handle domains like.co.ukwhich have 2.'s!