Skip to content

Instantly share code, notes, and snippets.

@jikamens
Last active February 13, 2024 10:54
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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.json 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 <jik@kamens.us>
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()
@shu8
Copy link

shu8 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!

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment