Last active
October 6, 2024 14:41
-
-
Save jikamens/3d8f018c47e4c4f0dd87b8c31bc57076 to your computer and use it in GitHub Desktop.
namecheap-dns.py - Export/import DNS records from/to Namecheap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks, fixed.