DDNS updater
ddns-updater source
Project updated · 16 April 2008
Download raw ddns-updater.py ↓
#!/usr/bin/env python
## DDNS (Dynamic DNS) zone updater
##
## Easy to use command line utility for creating
## and updating forward and reverse DNS entries
## in dynamically updatable domains.
##
## Supports zones on different servers, supports
## different keys for each zone, automatically
## creates reverse record and removes obsoleted
## ones.
##
## Example use:
## $ ddns-updater server.example.net 192.0.2.1
## will create a forward A record in example.net zone:
## server 3600 IN A 192.0.2.1
## and a reverse PTR record in 2.0.192.in-addr.arpa zone:
## 1 3600 IN A server.example.net.
##
## Author: Michal Ludvig <michal@logix.cz>
## /devel/ddns-updater
##
## License: GPL version 2
##
## TODO:
## * support for config file
## * IPv6 addresses support
## * support for other types of records (CNAME, MX, etc)
## ... anyone keen to help with these items? ;-)
import sys
import os
import random
import string
import re
ZONE_SERVER = {}
ZONE_KEY = {}
SERVER_KEY = {}
TTL_DEFAULT = 3600
NSUPDATE_PATH = "/usr/bin/nsupdate"
### ------ Put your local config here ------
ZONE_SERVER['default'] = "127.0.0.1"
#ZONE_SERVER['example.com'] = "192.0.2.123"
#ZONE_SERVER['example.net'] = "192.0.2.123"
#ZONE_KEY['example.com'] = '/etc/rndc.key'
#ZONE_KEY['example.net'] = 'example_net_key:NH1/N+RvZyrf1kH7/x5+c2MsLdlzL'
SERVER_KEY['127.0.0.1'] = "/var/named/named.keys"
### ------ Nothing to configure below this line ------
_rnd_chars = string.ascii_letters+string.digits
_rnd_chars_len = len(_rnd_chars)
def rndstr(len):
retval = ""
while len > 0:
retval += _rnd_chars[random.randint(0, _rnd_chars_len-1)]
len -= 1
return retval
def mktmpsomething(prefix, randchars, createfunc):
old_umask = os.umask(0077)
tries = 5
while tries > 0:
dirname = prefix + rndstr(randchars)
try:
createfunc(dirname)
break
except OSError, e:
if e.errno != errno.EEXIST:
os.umask(old_umask)
raise
tries -= 1
os.umask(old_umask)
return dirname
def mktmpdir(prefix = "/tmp/tmpdir-", randchars = 10):
return mktmpsomething(prefix, randchars, os.mkdir)
def mktmpfile(prefix = "/tmp/tmpfile-", randchars = 20):
createfunc = lambda filename : os.close(os.open(filename, os.O_CREAT | os.O_EXCL))
return mktmpsomething(prefix, randchars, createfunc)
def parse_key(key_file):
try:
key = open(key_file, "r").read()
pat = re.compile('key\s+(\w+)\s*{.*secret\s+[\'"]([^\'"]+)[\'"]', re.DOTALL | re.MULTILINE)
result = ":".join(pat.search(key).groups())
return result
except Exception, e:
sys.stderr.write("Can parse a key from %s: %s\n" % (key_file, e))
sys.exit(1)
def run_nsupdate(zone, args_in, mode):
"""
run_nsupdate(zone, args, mode)
zone is either forward or reverse DNS zone
args dict must have 'hostfqdn' and 'ipv4' members
mode is either 'forward' or 'reverse'
"""
# Use a copy, don't update the original
args = args_in.copy()
args['zone'] = zone
# Open tmp file for nsupdate script
args['tmpfilename'] = mktmpfile()
tmpfile = open(args['tmpfilename'], "w")
# Figure out server IP or name
args['server'] = None
if ZONE_SERVER.has_key(zone):
args['server'] = ZONE_SERVER[zone]
elif ZONE_SERVER.has_key('default'):
args['server'] = ZONE_SERVER['default']
# Find a key for this zone/server
args['key'] = None
args['key_param'] = None # key_param is a switch for nsupdate, -k for file, -y for inline key
if ZONE_KEY.has_key(zone):
args['key'] = ZONE_KEY[zone]
elif SERVER_KEY.has_key(args['server']):
args['key'] = SERVER_KEY[args['server']]
if args['key']:
if os.path.isfile(args['key']):
args['key'] = parse_key(args['key'])
# args['key_param'] = os.path.isfile(args['key']) and "-k" or "-y"
args['key_param'] = "-y"
# Generate the nsupdate script
if args['server']:
tmpfile.write("server %(server)s\n" % args)
tmpfile.write("zone %(zone)s\n" % args)
if mode == 'forward':
tmpfile.write("update delete %(hostfqdn)s.\n" % args)
tmpfile.write("update add %(hostfqdn)s. %(ttl)s IN A %(ipv4)s\n" % args)
elif mode == 'reverse':
tmpfile.write("update delete %(reverse_ipv4)s.\n" % args)
tmpfile.write("update add %(reverse_ipv4)s. %(ttl)s IN PTR %(hostfqdn)s.\n" % args)
tmpfile.write("show\n")
tmpfile.write("send\n")
tmpfile.close()
# Run nsupdate command
args['key_arg'] = args['key'] and "%(key_param)s %(key)s" % args or ""
command = "%(nsupdate)s %(key_arg)s %(tmpfilename)s" % args
print "Command: ", command
ret = os.system(command)
# Clean up
os.remove(args['tmpfilename'])
if __name__ == "__main__":
if len(sys.argv) < 3:
sys.stderr.write("Usage: %s <hostfqdn> <ipv4-addr>\n" % sys.argv[0])
sys.stderr.write("E.g.: %s server.example.net 192.0.2.1\n" % sys.argv[0])
sys.exit(1)
hostfqdn = sys.argv[1]
ipv4 = sys.argv[2]
host_parts = hostfqdn.split(".", 1)
ipv4_parts = ipv4.split(".")
if len(host_parts) != 2:
sys.stderr.write("Hostname '%s' is not fully qualified. Append your domain please.\n" % hostfqdn)
sys.exit(1)
if len(ipv4_parts) != 4:
sys.stderr.write("Invalid IPv4 address: %s\n" % ipv4)
sys.exit(1)
try:
ipv4_parts = [int(nibble) for nibble in ipv4_parts]
except ValueError:
sys.stderr.write("Invalid IPv4 address: %s\n" % ipv4)
sys.exit(1)
zone_fwd = host_parts[1]
zone_rev = "%d.%d.%d.in-addr.arpa" % (ipv4_parts[2], ipv4_parts[1], ipv4_parts[0])
args = {}
args['hostfqdn'] = hostfqdn
args['host'] = host_parts[1]
args['ipv4'] = ipv4
args['reverse_ipv4'] = "%d.%s" % (ipv4_parts[3], zone_rev)
args['ttl'] = TTL_DEFAULT
args['nsupdate'] = NSUPDATE_PATH
run_nsupdate(zone_fwd, args, 'forward')
run_nsupdate(zone_rev, args, 'reverse')