#! /usr/bin/env python # dnsupdate.py # A utility to update dynamic dns-records using a TSIG key. def checkerror(msg, show=""): # Determine if there are any errors reported if len(msg['error']) > 0: if show and not msg.has_key('quiet'): for error in msg['error']: print "Error: %s" % error print "Use -h option for help" return 1 else: return 0 def get_ipaddress(): # Connect to a remote server to determine which IP address # this host connects from import re import urllib opener = urllib.FancyURLopener({}) try: f = opener.open(msg['ipurl']) page = f.read() res = re.search('[12]?[0-9]?[0-9](\.[12]?[0-9]?[0-9]){3}', page) ip = res.group() return ip except: msg['error'].append("Could not determine IP address automatically,\n use -i switch to enter manually") def getparams(msg): # Read command line parameters and input values # OptionParser is part of Python 2.3 and called optik if installed under # Python 2.x try: from optparse import OptionParser except: from optik import OptionParser usage = "usage: %prog [OPTIONS]" parser = OptionParser(usage) parser.add_option("-c", "--config", type="string", help="Alternate config file") parser.add_option("--delete", action="store_true", help="Remove the host from the name server") parser.add_option("-d", "--domain", type="string", help="Domain to update") parser.add_option("--force", action="store_true", help="Force the action, do not check if update is necessary") parser.add_option("-i", "--ipaddress", type="string", help="Public IP address for this host [auto detected]") parser.add_option("-k", "--keyname", type="string", help="Name of the TSIG key") parser.add_option("-n", "--hostname", type="string", help="Hostname to publish") parser.add_option("--nameserver", type="string", help="IP address to the name server") parser.add_option("-q", "--quiet", action="store_true", help="Quit mode") parser.add_option("-s", "--keysecret", type="string", help="TSIG key") parser.add_option("--showcfg", action="store_true", help="Display config file data") parser.add_option("-u", "--ipurl", type="string", help="URL to the server which detects the public IP address") parser.add_option("-t", "--ttl", type="int", help="TTL in seconds") parser.add_option("-v", "--verbose", action="store_true", help="Print progress information") (options, args) = parser.parse_args() if options.config: msg['cfgfile'] = options.config msg = readcfg(msg) # Populate the basic params base_params = ['delete', 'domain', 'force', 'hostname', 'keyname', 'keysecret', 'nameserver', 'quiet', 'showcfg', 'ttl', 'ipurl'] for param in base_params: if eval('options.' + param): msg[param] = eval('options.' + param) # These parameters needs special handling if options.ipaddress: msg['ipaddress'] = options.ipaddress else: ip = get_ipaddress() if ip: msg['ipaddress'] = ip if options.verbose and msg.has_key('quiet'): del msg['quiet'] return msg def readcfg(msg, show=""): # Read the config file for pre configured values import os.path if not msg.has_key('cfgfile'): if os.path.exists(os.path.expanduser("~/.dnsupdaterc")): cfgfile = open(os.path.expanduser("~/.dnsupdaterc"), 'r') elif os.path.exists("/etc/dnsupdaterc"): cfgfile = open("/etc/dnsupdaterc", 'r') else: return msg else: if not os.path.exists(msg['cfgfile']): if show: checkerror(msg, show="errors") msg['error'].append("No such file; '%s'" % msg['cfgfile']) return msg cfgfile = open(msg['cfgfile'], 'r') if show: print "Current configuration values:" for line in cfgfile.readlines(): line = line.strip() if line.find("#", 0, 1) == 0 or not line: continue (key, value) = line.split('\t', 1) if show: print "%s: %s" % (key.strip(), value.strip()) if not value.strip().lower() == "false": msg[key.strip()] = value.strip() cfgfile.close() return msg def update(msg): # The update function connects to the dns server import dns.query import dns.tsigkeyring import dns.update import binascii # for exception handling in keyring generation import socket # for exception handling in server communictions # The name of the key and the secret try: keyring = dns.tsigkeyring.from_text({ msg['keyname']: msg['keysecret'] }) except binascii.Error: msg['error'].append("Your password is incorrect.") return # dns.update.Update(name of domain, keyring, keyname) update = dns.update.Update(msg['domain'], keyring=keyring, keyname=msg['keyname']) if msg.has_key('delete'): update.delete(msg['hostname']) else: # update.replace(hostname, ttl, record-type, new ip) update.replace(msg['hostname'], msg['ttl'], 'a', msg['ipaddress']) # doit, servername try: response = dns.query.tcp(update, msg['nameserver']) except socket.error: msg['error'].append("An error occurred in the server communication.") return except dns.tsig.BadSignature: msg['error'].append("Your password could not be verified.\n Check your password and keyname.") return # Verify response if not msg.has_key('quiet'): if response.rcode() == 0: if msg.has_key("delete"): print "Host '%s.%s' has been deleted" % (msg['hostname'], msg['domain']) else: print "Host '%s.%s' has been added with IP address %s" % (msg['hostname'], msg['domain'], msg['ipaddress']) else: msg['error'].append("Update denied, server responded %s" % dns.rcode.to_text(response.rcode())) def validate(msg): import re import string # Verify all required data is present and sanity check incoming data req_vals = ['domain', 'hostname', 'ipaddress', 'keyname', 'keysecret', 'nameserver', 'ttl'] for value in req_vals: if not msg.has_key(value): msg['error'].append('Missing "%s" parameter' % value) #global hostname, ipaddress, ttl if msg.has_key('hostname') and msg.has_key('domain'): msg['hostname'] = string.replace(msg['hostname'], "." + msg['domain'], '') if msg.has_key('ipaddress'): if not re.search('^[12]?[0-9]?[0-9](\.[12]?[0-9]?[0-9]){3}$', msg['ipaddress']): msg['error'].append("Invalid IP address '%s'" % msg['ipaddress']) return msg def verify_ip(msg): # Check if the ip address exists and if it needs an update import dns.resolver ip = "" host = msg['hostname'] + "." + msg['domain'] try: res = dns.resolver.Resolver() res.nameservers = [msg['nameserver']] ans = res.query(host) for res in ans: ip = res.to_text() except dns.exception.Timeout: msg['error'].append("Connection timeout, could not connect to name server.\n") except dns.resolver.NXDOMAIN: pass if ip == msg['ipaddress'] and not msg.has_key('delete'): msg['error'].append("Name server already up to date") elif ip == "" and msg.has_key('delete'): msg['error'].append("Name server does not recognise the hostname") return msg if __name__ == "__main__": import sys import syslog msg = {} msg['error'] = [] getparams(msg) if msg.has_key('showcfg'): readcfg(msg, show="config") sys.exit(0) validate(msg) err = checkerror(msg) if err == 0: if not msg.has_key('force'): verify_ip(msg) err = checkerror(msg) if err == 0: update(msg) err = checkerror(msg) if err == 0: syslog.openlog("dnsupdate") for error in msg['error']: syslog.syslog(syslog.LOG_ERR, error) syslog.closelog() sys.exit(0) err = checkerror(msg, show="errors") syslog.openlog("dnsupdate") for error in msg['error']: syslog.syslog(syslog.LOG_ERR, error) syslog.closelog() sys.exit(1)