-
-
Save pklaus/4619865 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python2.7 | |
# Matt's DNS management tool | |
# Manage DNS using DDNS features | |
# | |
# See http://planetfoo.org/blog/archive/2012/01/24/a-better-nsupdate/ | |
# | |
# Usage: dnsupdate -s server -k key -t ttl add _minecraft._tcp.mc.example.com SRV 0 0 25566 mc.example.com. | |
# -h HELP! | |
# -s the server | |
# -k the key | |
# -t the ttl | |
# the action (add, delete, replace) and record specific parameters | |
import argparse | |
import textwrap | |
import re | |
import dns.query | |
import dns.tsigkeyring | |
import dns.update | |
import dns.reversename | |
import dns.resolver | |
from dns.exception import DNSException, SyntaxError | |
Verbose = False | |
# | |
# Let's use argparser! | |
def getArgs(): | |
# Setup a argument parser to collect the values we need | |
Args = argparse.ArgumentParser(usage='%(prog)s [-h] {-s} {-k} {-o} [-x] {add|delete|update} {Name} {TTL} [IN] {Type} {Target}', description='Add, Delete, Replace DNS records using DDNS.') | |
# -s - The Server | |
Args.add_argument('-s', dest='Server', required=True, | |
help='DNS server to update (Required)') | |
# -k - The Key | |
Args.add_argument('-k', dest='Key', required=True, | |
help='TSIG key. The TSIG key file should be in DNS KEY record format. (Required)') | |
# -o - The Origin | |
Args.add_argument('-o', dest='Origin', required=False, | |
help='Specify the origin. Optional, if not provided origin will be determined') | |
# -x - Add Reverse? | |
Args.add_argument('-x', dest='doPTR', action='store_true', | |
help='Also modify the PTR for a given A or AAAA record. Forward and reverse zones must be on the same server.') | |
# -v - Verbose? | |
Args.add_argument('-v', dest='Verbose', action='store_true', | |
help='Print the rcode returned with for each update') | |
# -t - The TTL | |
Args.add_argument('-t', dest='TimeToLive', required=False, default="600", | |
help='Specify the TTL. Optional, if not provided TTL will be default to 600.') | |
# myInput is a list of additional values required. Actual data varies based on action | |
Args.add_argument('myInput', action='store', nargs='+', metavar='add|delete|update', | |
help='{hostname} [IN] {Type} {Target}.') | |
myArgs = Args.parse_args() | |
return myArgs | |
# | |
# Is a valid TTL? | |
def isValidTTL(TTL): | |
try: | |
TTL = dns.ttl.from_text(TTL) | |
except: | |
print 'TTL:', TTL, 'is not valid' | |
exit() | |
return TTL | |
# | |
# Is a Valid PTR? | |
def isValidPTR(ptr): | |
if re.match(r'\b(?:\d{1,3}\.){3}\d{1,3}.in-addr.arpa\b', ptr): | |
return True | |
else: | |
print 'Error:', ptr, 'is not a valid PTR record' | |
exit() | |
# | |
# Is a valid IPV4 address? | |
def isValidV4Addr(Address): | |
try: | |
dns.ipv4.inet_aton(Address) | |
except socket.error: | |
print 'Error:', Address, 'is not a valid IPv4 address' | |
exit() | |
return True | |
# | |
# Is a valid IPv6 address? | |
def isValidV6Addr(Address): | |
try: | |
dns.ipv6.inet_aton(Address) | |
except SyntaxError: | |
print 'Error:', Address, 'is not a valid IPv6 address' | |
exit() | |
return True | |
def isValidName(Name): | |
if re.match(r'^(([a-zA-Z0-9]|[a-zA-Z0-9\_][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]\.?)$', Name): | |
return True | |
else: | |
print 'Error:', Name, 'is not a valid name' | |
exit() | |
def verifymyInput(myInput): | |
# Validate the host and domain name syntax | |
# We're going to make sure that the action and arguments in myInput are valid | |
action = myInput[0].lower() | |
if action != 'add' and action != 'delete' and action != 'del' and action != 'update': | |
print 'FATAL: Invalid action' | |
print 'Usage: dnsupdate [-o origin] -s server -k key [-t ttl] [add|delete|update] [Name] [Type] [Address]' | |
exit() | |
if action == 'delete' or action == 'del': # skip type checks | |
return 'del', None # Bail out early | |
# We need to know type in order to do some tests so we'll define it here | |
type = myInput[2].upper() | |
# Based on the type of record we're trying to update we'll run some tests | |
if type == 'A' or type == 'AAAA': | |
if len(myInput) < 4: | |
print 'FATAL: not enough options for an A record' | |
print 'Usage: dnsupdate -o origin -s server -k key [-t ttl] add|delete|update Name A Address' | |
exit() | |
isValidName(myInput[1]) | |
if type == 'A': | |
isValidV4Addr(myInput[3]) | |
elif type == 'AAAA': | |
isValidV6Addr(myInput[3]) | |
if type == 'CNAME' or type == 'NS': | |
if len(myInput) < 4: | |
print 'FATAL: not enough options for a CNAME record' | |
print 'Usage: dnsupdate -o origin -s server -k key [-t ttl] add|delete|update Name CNAME Target' | |
exit() | |
isValidName(myInput[1]) | |
isValidName(myInput[3]) | |
if type == 'PTR': | |
if len(myInput) < 4: | |
print 'Error: not enough options for a PTR record' | |
print 'Usage: dnsupdate -o origin -s server -k key [-t ttl] add|delete|update Name PTR Target' | |
exit() | |
# isValidPTR(myInput[1]) | |
isValidName(myInput[3]) | |
if type == 'TXT': | |
# Wrap the TXT string in quotes since the quotes get stripped | |
myInput[3] = '"%s"' % myInput[3] | |
if type == 'MX': | |
if len(myInput) < 4: | |
print 'Error: not enough options for an MX record' | |
print 'Usage: dnsupdate -o origin -s server -k key [-t ttl] add|delete|update Name MX Weight Target' | |
if int(myInput[3]) > 65535 or int(myInput[3]) < 0: | |
print 'Error: Preference must be between 0 - 65535' | |
exit() | |
isValidName(myInput[1]) | |
isValidName(myInput[5]) | |
if type == 'SRV': | |
if len(myInput) < 6: | |
print 'Error: not enough options for a SRV record' | |
print 'Usage: dnsupdate -o origin -s server -k key [-t ttl] add|delete|update Name SRV Priority Weight Port Target' | |
if int(myInput[3]) > 65535 or int(myInput[3]) < 0: | |
print 'Error: Priority must be between 0 - 65535' | |
exit() | |
if int(myInput[4]) > 65535 or int(myInput[4]) < 0: | |
print 'Error: Weight must be between 0 - 65535' | |
exit() | |
if int(myInput[5]) > 65535 or int(myInput[5]) < 0: | |
print 'Error: Port must be between 0 - 65535' | |
exit() | |
isValidName(myInput[1]) | |
isValidName(myInput[6]) | |
return action, type | |
def getKey(FileName): | |
f = open(FileName) | |
keyfile = f.read().splitlines() # Fixed by Kamilion 7/7/13 | |
f.close() | |
hostname = keyfile[0].rsplit(' ')[1].replace('"', '').strip() | |
algo = keyfile[1].rsplit(' ')[1].replace(';','').replace('-','_').upper().strip() | |
key = keyfile[2].rsplit(' ')[1].replace('}','').replace(';','').replace('"', '').strip() | |
k = {hostname:key} | |
try: | |
KeyRing = dns.tsigkeyring.from_text(k) | |
except: | |
print k, 'is not a valid key. The file should be in DNS KEY record format. See dnssec-keygen(8)' | |
exit() | |
return [KeyRing, algo] | |
def genPTR(Address): | |
try: | |
a = dns.reversename.from_address(Address) | |
except: | |
print 'Error:', Address, 'is not a valid IP adresss' | |
return a | |
def parseName(Origin, Name): | |
try: | |
n = dns.name.from_text(Name) | |
except: | |
print 'Error:', n, 'is not a valid name' | |
exit() | |
if Origin is None: | |
Origin = dns.resolver.zone_for_name(n) | |
Name = n.relativize(Origin) | |
return Origin, Name | |
else: | |
try: | |
Origin = dns.name.from_text(Origin) | |
except: | |
print 'Error:', Name, 'is not a valid origin' | |
exit() | |
Name = n - Origin | |
return Origin, Name | |
def doUpdate(Server, KeyFile, Origin, TimeToLive, doPTR, myInput): | |
# if the Class is defined (e.g. IN) strip it out | |
if len(myInput) > 2 and myInput[2].upper() == 'IN': | |
myInput.pop(2) | |
# Sanity check the data and get the action and record type | |
Action, Type = verifymyInput(myInput) | |
TTL = isValidTTL(TimeToLive) | |
# Get the hostname and the origin | |
Origin, Name = parseName(Origin, myInput[1]) | |
# Validate and setup the Key | |
KeyRing, KeyAlgo = getKey(KeyFile) | |
# Start constructing the DDNS Query | |
Update = dns.update.Update(Origin, keyring=KeyRing, keyalgorithm=getattr(dns.tsig, KeyAlgo)) # fixed by Kamilion 7/7/13 | |
# Put the payload together. | |
myPayload = '' # Start with an empty payload. | |
if Type == 'A' or Type == 'AAAA': | |
myPayload = myInput[3] | |
if doPTR == True: | |
ptrTarget = Name.to_text() + '.' + Origin.to_text() | |
ptrOrigin, ptrName = parseName(None, genPTR(myPayload).to_text()) | |
ptrUpdate = dns.update.Update(ptrOrigin, keyring=KeyRing) | |
if Action != 'del' and Type == 'CNAME' or Type == 'NS' or Type == 'TXT' or Type == 'PTR': | |
myPayload = myInput[3] | |
do_PTR = False | |
elif Type == 'SRV': | |
myPayload = myInput[3]+' '+myInput[4]+' '+myInput[5]+' '+myInput[6] | |
do_PTR = False | |
elif Type == 'MX': | |
myPayload = myInput[3]+' '+myInput[4] | |
do_PTR = False | |
# Build the update | |
if Action == 'add': | |
Update.add(Name, TTL, Type, myPayload) | |
if doPTR == True: | |
ptrUpdate.add(ptrName, TTL, 'PTR', ptrTarget) | |
elif Action == 'delete' or Action == 'del': | |
if myPayload != '': | |
Update.delete(Name, Type, myPayload) | |
else: | |
Update.delete(Name) | |
if doPTR == True: | |
ptrUpdate.delete(ptrName, 'PTR', ptrTarget) | |
elif Action == 'update': | |
Update.replace(Name, TTL, Type, myPayload) | |
if doPTR == True: | |
ptrUpdate.replace(ptrName, TTL, 'PTR', ptrTarget) | |
# Do the update | |
try: | |
Response = dns.query.tcp(Update, Server) | |
except dns.tsig.PeerBadKey: | |
print 'ERROR: The server is refusing our key' | |
exit() | |
except dns.tsig.PeerBadSignature: | |
print 'ERROR: Something is wrong with the signature of the key' | |
exit() | |
if Verbose == True: | |
print 'Manipulating', Type, 'record for', Name, 'resulted in:', dns.rcode.to_text(Response.rcode()) | |
if doPTR == True: | |
try: | |
ptrResponse = dns.query.tcp(ptrUpdate, Server) | |
except dns.tsig.PeerBadKey: | |
print 'ERROR: The server is refusing our key' | |
exit() | |
except dns.tsig.PeerBadSignature: | |
print 'ERROR: Something is wrong with the signature of the key' | |
exit() | |
if Verbose == True: | |
print 'Creating PTR record for', Name, 'resulted in:', dns.rcode.to_text(Response.rcode()) | |
#print 'completed.' | |
def main(): | |
myArgs = getArgs() | |
global Verbose | |
if myArgs.Verbose == True: | |
Verbose = True | |
doUpdate(myArgs.Server, myArgs.Key, myArgs.Origin, myArgs.TimeToLive, myArgs.doPTR, myArgs.myInput) | |
main() |
@ryanczak If your blog post used to live at http://planetfoo.org/blog/archive/2012/01/24/a-better-nsupdate/ it's mentioned in the header of the script, but I am getting 'connection refused' from that site.
Anyway, thanks y'all - I'll see if I can make use of this :)
My blog died from neglect :) Something lives on though so 👍
Guys;
I am running the script on Centos 7.5 and got the error:
[root@nsserver1 zague]# ./dnsupdate.py -s nsserver1.kpt.com.mx -k /var/named/data/Kkpt.com.mx.+157+56738.key delete mail.kpt.com.mx. IN A Traceback (most recent call last): File "./dnsupdate.py", line 290, in <module> main() File "./dnsupdate.py", line 288, in main doUpdate(myArgs.Server, myArgs.Key, myArgs.Origin, myArgs.TimeToLive, myArgs.doPTR, myArgs.myInput) File "./dnsupdate.py", line 223, in doUpdate KeyRing, KeyAlgo = getKey(KeyFile) File "./dnsupdate.py", line 176, in getKey algo = keyfile[1].rsplit(' ')[1].replace(';','').replace('-','_').upper().strip() IndexError: list index out of range
Any Ideas
@ryanczak and @epleterte, thanks.
For those interested, the related blog post is archived at:
Just wondering whether it works today as @vazquc reported above at https://gist.github.com/pklaus/4619865#gistcomment-2766052?!
Season's greetings and cheers,
/z
Hi,
Tried to use, but getting following error, no matter whether I specify -t flag or not:
$ python3 dnsupdate.py -s ns2.mydomain.net -k Kns2.mydomain.net.+165+00940.key add test.myzone.com 3600 A xx.yy.zz.abc
File "dnsupdate.py", line 68
print 'TTL:', TTL, 'is not valid'
^
SyntaxError: Missing parentheses in call to 'print'
Any input?
Cheers,
/z
@ryanczak and @epleterte, thanks.
For those interested, the related blog post is archived at:
Just wondering whether it works today as @vazquc reported above at https://gist.github.com/pklaus/4619865#gistcomment-2766052?!
Season's greetings and cheers,
/z
I was not able to fix the error, fortunately I found this other script https://gist.github.com/4707775 and it's working perfectly.
Regards
Funny to see this here. I guess it came from my old blog? Happy to see that it is useful to someone. Perhaps I should commit some of my other scripts to github....