Skip to content

Instantly share code, notes, and snippets.

@jikamens
Last active October 6, 2024 14:41
Show Gist options
  • Save jikamens/3d8f018c47e4c4f0dd87b8c31bc57076 to your computer and use it in GitHub Desktop.
Save jikamens/3d8f018c47e4c4f0dd87b8c31bc57076 to your computer and use it in GitHub Desktop.
namecheap-dns.py - Export/import DNS records from/to Namecheap
#!/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()
@jikamens
Copy link
Author

jikamens commented Aug 7, 2023

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.uk which have 2 .'s!

Thanks, fixed!

@thegiantbeast
Copy link

instead of yaml file maybe a better option could be the bind format

@fergbrain
Copy link

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.

@jikamens
Copy link
Author

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.

@jikamens
Copy link
Author

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.

Thanks, fixed.

@mobeigi
Copy link

mobeigi commented Oct 6, 2024

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