#!/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')