Now using common base class for exceptions.
[toast/tdyndns.git] / bin / tdyndns_update
1 #!/usr/bin/python
2 import sys
3 import re
4 import argparse
5 from subprocess import Popen, PIPE, call
6 import ipaddr
7
8
9 class ExternalProgramError(RuntimeError):
10         pass
11
12
13 class NsupdateError(ExternalProgramError):
14         def __init__(self, returncode):
15                 self.returncode = returncode
16
17
18 class BlockipError(ExternalProgramError):
19         def __init__(self, returncode):
20                 self.returncode = returncode
21
22
23 def ipfamily_by_ip(ip):
24         if isinstance(ip, ipaddr.IPv4Address):
25                 return 'A'
26         elif isinstance(ip, ipaddr.IPv6Address):
27                 return 'AAAA'
28         assert False
29
30
31 def nsupdate_add(fqdn, ttl, ip):
32         """
33         :param fqdn: Fully qualified domain name
34         :param ip_family: A or AAAA
35         :raises an NsupdateError in case of errors."""
36         command = "update add {fqdn} {ttl} IN {ip_family} {ip}\n\n".format(fqdn=fqdn, ttl=ttl, ip_family=ipfamily_by_ip(ip), ip=ip)
37         p = Popen(['nsupdate', '-l'], stdin=PIPE)
38         p.communicate(command)
39         if p.returncode != 0:
40                 raise NsupdateError(p.returncode)
41
42 def nsupdate_delete(fqdn, ip_family):
43         """
44         :param fqdn: Fully qualified domain name
45         :param ip_family: A or AAAA
46         :raises an NsupdateError in case of errors."""
47         command = "update delete {fqdn} {ip_family}\n\n".format(fqdn=fqdn, ip_family=ip_family)
48         p = Popen(['nsupdate', '-l'], stdin=PIPE)
49         p.communicate(command)
50         if p.returncode != 0:
51                 raise NsupdateError(p.returncode)
52
53
54 def blockip_whitelist_add(ip):
55         """
56         :param ip: ipv4 address
57         :raises a BlockipError in case of errors."""
58         command = "iptables -I blockip -s {ip} -j ACCEPT".format(ip=ip)
59         p = call(command, shell=True)
60         if p != 0:
61                 raise BlockipError(p)
62
63
64 def blockip_whitelist_delete(ip):
65         """
66         :param ip: ipv4 address
67         :raises a BlockipError in case of errors."""
68         command = "iptables -D blockip -s {ip} -j ACCEPT".format(ip=ip)
69         p = call(command, shell=True)
70         if p != 0:
71                 raise BlockipError(p)
72
73
74 def main(args):
75         try:
76                 if args.delete:
77                         if args.ip is None:
78                                 nsupdate_delete(args.fqdn, 'A')
79                                 nsupdate_delete(args.fqdn, 'AAAA')
80                         else:
81                                 nsupdate_delete(args.fqdn, ipfamily_by_ip(args.ip))
82                                 if ipfamily_by_ip(args.ip) == 'A':
83                                         blockip_whitelist_delete(args.ip)
84                 else:
85                         nsupdate_delete(args.fqdn, ipfamily_by_ip(args.ip))
86                         nsupdate_add(args.fqdn, args.ttl, args.ip)
87                         if ipfamily_by_ip(args.ip) == 'A':
88                                 blockip_whitelist_add(args.ip)
89         except ExternalProgramError as e:
90                 sys.exit(e.returncode)
91
92
93
94 if __name__ == '__main__':
95         parser = argparse.ArgumentParser(description='Add or delete a domain name from dyndns (simplifies call to nsupdate).')
96         parser.add_argument('-d', '--delete', action='store_true', help='delete instead of add')
97         parser.add_argument('-i', '--ip', help='IP address (either IPv4 or IPv6)')
98         parser.add_argument('-t', '--ttl', type=int, default=600, help='TTL (default: 600)')
99         parser.add_argument('fqdn', help='fully qualified domain name to add or delete, e.g. myserver.dyn.example.com')
100         args = parser.parse_args()
101
102         # check ip
103         if not args.delete and not args.ip:
104                 parser.error('The IP address is mandatory')
105         if args.ip:
106                 try:
107                         args.ip = ipaddr.IPAddress(args.ip) # throws an exception if the IP address is not valid
108                 except ValueError:
109                         parser.error('The IP address is not valid')
110
111         # check fqdn
112         if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', args.fqdn) is None:
113                 parser.error('The fqdn has an invalid format.')
114
115         main(args)