Files
dnsupdate/dnsupdate.py

292 lines
8.4 KiB
Python

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