#!/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>
##         http://www.logix.cz/michal/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')

