DHCPD config to DNS update script
This one takes some explaining. Here is the scenario in which it's used:
The ISC DHCPD supports dynamic update of DNS zones, which is handy.
- On our network some hosts have static IPs, but DHCPD won't generate any DNS updates for these hosts.
- To keep everyone's sanity, we would like all IP/name configuration in the same place and same config file.
This script parses static host entries in the dhcpd.conf file, and generates DNS update data suitable for use with the nsupdate tool. It handles entries with both a fixed-address and ddns-hostname option, like so:
host zarquon {
ddns-hostname "zarquon";
hardware ethernet 00:08:74:CE:3A:A0;
fixed-address 10.13.0.124;
}It keeps a status file containing the last updates performed, so it only generates updates for hosts that have changed. We run it after restarting DHCPD with this script:
The code itself is pretty hairy; it's a badly hacked up version of a dhcpd.conf parser I found in a GUI program. Here it is; obviously you will need to adjust the FWDZONE, REVZONE and TTL values to suit your environment:
1 #!/usr/bin/python
2 ## dhcpdconfobject.py - Object of dictionaries of settings from /etc/dhcpd.conf
3 ## Copyright (C) 2004 Kevin M. Gill <kmg_usmc@yahoo.com>
4 ## Ripped out and hacked up by Andrew Baumann 2005
5
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
10
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ## GNU General Public License for more details.
15
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
20 import os, sys, re, string
21
22 FWDZONE = "keg.ertos.in.nicta.com.au"
23 REVZONE = "13.10.in-addr.arpa"
24 TTL = "10800"
25
26 class dhcpdconf:
27 dhcpdConfFile = ""
28 general_options = {}
29 general_options["subnet"] = {}
30 allowordeny = {}
31 allowordeny["unknown-clients"] = "allow"
32 allowordeny["bootp"] = "allow"
33 groups = {}
34 hosts = {}
35 subnets = {}
36 args = ""
37 networks = {}
38
39
40 def __init__(self, fileName):
41 self.dhcpdConfFile = fileName
42
43 in_brackets = 0
44 in_type = 0 # 0 -> General Options
45 # 1 -> Groups
46 # 2 -> Hosts within groups
47 # 3 -> individual hosts
48 # 4 -> subnet options(ranges)
49 # 5 -> Hosts within a subnet
50
51 type_count = {0 : 0, 1 : -1, 2 : 0, 3 : 0, 4 : -1, 5 : 0}
52 inputs = []
53 inputs.append(open(self.dhcpdConfFile, 'r'))
54
55 while inputs != []:
56 line = ''
57 while line == '':
58 if inputs == []:
59 break
60 line = inputs[-1].readline()
61 if line == '':
62 inputs.pop().close()
63
64 #line = line[0:len(line)]
65 chop = self.__str_to_columns(string.strip(line))
66
67 if chop != 0:
68 if (chop[0] == "deny" or chop[0] == "allow") and in_type == 0:
69 if self.allowordeny.has_key(chop[1]):
70 if chop.has_key(1):
71 self.allowordeny[chop[1]] = chop[0]
72 elif self.__fndsubany("{", line):
73 if chop[0] == "subnet":
74 i = 0
75 for key in self.subnets:
76 i = i + 1
77 self.subnets[i] = {}
78 self.subnets[i]["hosts"] = {}
79 self.subnets[i]["network_address"] = chop[1]
80 self.subnets[i]["name"] = chop[1]
81 self.subnets[i]["network_mask"] = chop[3]
82 type_count[4] = type_count[4] + 1
83 in_type = 4
84 if chop[0] == "group":
85 in_brackets = 1
86 in_type = 1
87 type_count[1] = type_count[1] + 1
88 self.groups[type_count[1]] = {}
89 self.groups[type_count[1]]["hosts"] = {}
90 comment = string.find(line, "#")
91 if comment != -1:
92 self.groups[type_count[1]]["name"] = string.strip(line[(comment + 1):len(line)])
93 else:
94 self.groups[type_count[1]]["name"] = "Unnamed_" + str(type_count[1])
95 if chop[0] == "host":
96 if in_type == 4: # In Subnet
97 in_brackets = 1
98 in_type = 5
99 key_vol = self.__key_volume(self.subnets[type_count[4]]["hosts"])
100 self.subnets[type_count[4]]["hosts"][key_vol] = {}
101 self.subnets[type_count[4]]["hosts"][key_vol]["name"] = chop[1]
102 if chop.has_key(2) and chop.has_key(3):
103 self.subnets[type_count[4]]["hosts"][key_vol][chop[3]] = chop[4]
104 for key in chop:
105 if chop[key] == "}":
106 in_type = 4
107 #line = string.replace(line, "}", "")
108
109 if in_type == 1: # In group
110 in_brackets = 1
111 in_type = 2
112 key_vol = self.__key_volume(self.groups[type_count[1]]["hosts"])
113 self.groups[type_count[1]]["hosts"][key_vol] = {}
114 self.groups[type_count[1]]["hosts"][key_vol]["name"] = chop[1]
115 if chop.has_key(2) and chop.has_key(3):
116 self.groups[type_count[1]]["hosts"][key_vol][chop[2]] = chop[3]
117 if in_type == 0: # Individual
118 in_brackets = 1
119 in_type = 3
120 key_vol = self.__key_volume(self.hosts)
121 self.hosts[key_vol] = {}
122 self.hosts[key_vol]["name"] = chop[1]
123 if chop.has_key(2) and chop.has_key(3):
124 self.hosts[key_vol][chop[2]] = chop[3]
125
126 elif self.__fndsubany("}", line):
127 if in_type == 2:
128 in_type = 1
129 elif in_type == 5:
130 in_type = 4
131 else:
132 in_type = 0
133 in_brackets = 0
134 else:
135 if chop[0] == "include":
136 try:
137 inputs.append(open(chop[1], 'r'))
138 except IOError, (errno, strerror):
139 sys.stderr.write("Warning: couldn't open %s: %s\n" % (chop[1], strerror))
140 continue
141 if in_type == 0:
142 key, value = self.__set_option(chop)
143 self.general_options[key] = value
144
145 if in_type == 1:
146 key, value = self.__set_option(chop)
147 self.groups[type_count[1]][key] = value
148
149 if in_type == 2:
150 key_vol = self.__key_volume(self.groups[type_count[1]]["hosts"])
151 key, value = self.__set_option(chop)
152 self.groups[type_count[1]]["hosts"][key_vol - 1][key] = value
153 if in_type == 5:
154 key_vol = self.__key_volume(self.subnets[type_count[4]]["hosts"])
155 key, value = self.__set_option(chop)
156 self.subnets[type_count[4]]["hosts"][key_vol - 1][key] = value
157 if in_type == 3:
158 key_vol = self.__key_volume(self.hosts)
159 key, value = self.__set_option(chop)
160 self.hosts[key_vol - 1][key] = value
161
162 if in_type == 4:
163 key_vol_nets = self.__key_volume(self.subnets) - 1
164 key_vol = self.__key_volume(chop)
165 if self.__fndsubany("}", line):
166 in_type = 0
167 in_brackets = 0
168 elif chop[0] == "range":
169 i = 1
170
171 self.subnets[key_vol_nets]["range"] = {}
172 if key_vol == 4:
173 i = i + 1
174 self.subnets[key_vol_nets]["range"]["bootp"] = chop[1]
175 else:
176 self.subnets[key_vol_nets]["range"]["bootp"] = ""
177 self.subnets[key_vol_nets]["range"]["start"] = chop[i]
178 self.subnets[key_vol_nets]["range"]["end"] = chop[i + 1]
179 else:
180 if key_vol > 2:
181 self.subnets[key_vol_nets][chop[0]] = {}
182 for i in range(key_vol):
183 if i > 0:
184 self.subnets[key_vol_nets][chop[0]][i - 1] = chop[i]
185 i = i + 1
186
187 else:
188 #self.subnets[key_vol_nets][chop[0]] = string.replace(string.replace(chop[1], ";", ""), "\"", "")
189 self.subnets[key_vol_nets][chop[0]] = string.strip(string.strip(chop[1], ";"), "\"")
190
191
192 def add_host(self, options, group):
193 if group == -1:
194 key_vol = self.__key_volume(self.hosts)
195 self.hosts[key_vol] = options
196 else:
197 key_vol = self.__key_volume(self.groups[group]["hosts"])
198 self.groups[group]["hosts"][key_vol] = options
199
200
201 def __set_option(self, chop):
202 tmp_list = {}
203 option_key = 0
204 #if chop[0] == "option":
205 # option_key = 1 # the location of the option not the WORD option
206 #else:
207 # option_key = 0
208 key_vol = self.__key_volume(chop)
209 if key_vol == option_key + 2:
210 tmp_list[chop[option_key]] = chop[option_key + 1]
211 else:
212 tmp_list[chop[option_key]] = {}
213 i = 0
214 j = 0
215 for i in range(key_vol):
216 if i > option_key:
217 tmp_list[chop[option_key]][j] = chop[i]
218 j+=1
219 return chop[option_key], tmp_list[chop[option_key]]
220
221
222 def __key_volume(self, dict):
223 i = 0
224 for temp in dict:
225 i = i + 1
226 try:
227 if temp >= i: i = temp + 1
228 except:
229 pass
230 return i
231
232
233 def __fndsubany(self, str, set):
234 return 1 in [c in str for c in set]
235
236
237 def __fndsub(self, needle, haystack):
238 for i in range(len(haystack)):
239 if haystack[i:i+len(needle)] == needle:
240 return 1
241
242
243 def __str_to_columns(self, line):
244 comment = string.find(line, "#")
245
246 if comment > 0:
247 line = line[0:comment - 1]
248 if comment == 0:
249 line = ""
250 return 0
251 protochop = re.split("[ ]", line)
252 if re.compile("^ ?$").search(line, 1):
253 return 0
254 chop = {}
255 i = 0
256 key = ""
257 lkey = ""
258 for key in protochop:
259 key = string.strip(key)
260 if (not re.compile("^ \n").search(key, 1)) & (len(key) > 0):
261 tmpkey = string.replace(string.replace(key, "\"", ""), ";", "")
262 if i > 0:
263 oldkey = string.replace(string.replace(chop[i - 1], "\"", ""), ";", "")
264 dblkey = oldkey + " " + tmpkey
265 if oldkey == "option":
266 chop[i - 1] = dblkey
267 elif dblkey != "hardware ethernet":
268 chop[i] = tmpkey
269 i+=1
270 else:
271 chop[i - 1] = dblkey
272 else:
273 chop[i] = tmpkey
274 i+=1
275 lkey = key
276 return chop
277
278
279 def _verify_ip(self, text):
280 if string.count(text, ".") == 3:
281 octets = string.split(text, ".")
282 for i in range(4):
283 if i >= 0:
284 try:
285 test = int(octets[i])
286 if test < 0 or test > 255:
287 return 0
288 except:
289 return 0
290 fixed = str(int(octets[0])) + "." + str(int(octets[1])) + "." + str(int(octets[2])) + "." + str(int(octets[3]))
291 return fixed
292 else:
293 return 0
294
295
296 def parseconfig(filename):
297 dc = dhcpdconf(filename)
298 ipaddrs = {}
299 for n in range(len(dc.groups)):
300 group = dc.groups[n]
301 for m in range(len(group['hosts'])):
302 host = group['hosts'][m]
303 if host.has_key('fixed-address') and host.has_key('ddns-hostname'):
304 ipaddrs[host['ddns-hostname']] = host['fixed-address']
305 for n in range(len(dc.subnets)):
306 subnet = dc.subnets[n]
307 for m in range(len(subnet['hosts'])):
308 host = subnet['hosts'][m]
309 if host.has_key('fixed-address') and host.has_key('ddns-hostname'):
310 ipaddrs[host['ddns-hostname']] = host['fixed-address']
311 for n in range(len(dc.hosts)):
312 host = dc.hosts[n]
313 if host.has_key('fixed-address') and host.has_key('ddns-hostname'):
314 ipaddrs[host['ddns-hostname']] = host['fixed-address']
315
316 return ipaddrs
317
318
319 def parsestatus(filename):
320 ipaddrs = {}
321 fh = open(filename, 'r')
322 for line in fh:
323 data = line.split()
324 if len(data) != 2:
325 sys.stderr.write("Error: failed to parse %s\n", filename)
326 sys.exit(1)
327 ipaddrs[data[0]] = data[1]
328 fh.close()
329 return ipaddrs
330
331 def writestatus(filename, ipaddrs):
332 fh = open(filename, 'w')
333 keys = ipaddrs.keys()
334 keys.sort()
335 for host in keys:
336 fh.write("%s\t%s\n" % (host, ipaddrs[host]))
337 fh.close()
338
339
340 def finddeleted(ipaddrs, oldips):
341 delhosts = []
342 delips = []
343 for (host, ip) in oldips.items():
344 if host not in ipaddrs.keys():
345 delhosts.append(host)
346 if ip not in ipaddrs.values():
347 delips.append(ip)
348 return delhosts, delips
349
350
351 def makearpa(ip):
352 octets = ip.split('.')
353 assert(len(octets) == 4)
354 octets.reverse()
355 return ("%s.%s.%s.%s.in-addr.arpa" % tuple(octets))
356
357
358 def genoutput(ipaddrs, deletedhosts, deletedips):
359 print "zone %s" % FWDZONE
360 deletedhosts.sort()
361 for host in deletedhosts:
362 print "update delete %s.%s." % (host, FWDZONE)
363 keys = ipaddrs.keys()
364 keys.sort()
365 for host in keys:
366 print "update delete %s.%s." % (host, FWDZONE)
367 print "update add %s.%s. %s A %s" % (host, FWDZONE, TTL, ipaddrs[host])
368
369 # reverse the ipaddrs hash
370 hostnames = {}
371 for (host, ip) in ipaddrs.items():
372 hostnames[ip] = host
373
374 print "send"
375 print "zone %s" % REVZONE
376 deletedips.sort()
377 for ip in deletedips:
378 print "update delete %s." % makearpa(ip)
379 keys = hostnames.keys()
380 keys.sort()
381 for ip in keys:
382 print "update delete %s." % makearpa(ip)
383 print "update add %s. %s PTR %s.%s." % (makearpa(ip), TTL, hostnames[ip], FWDZONE)
384 print "send"
385
386 if __name__ == "__main__":
387 # parse the config file and extract hostnames and IPs
388 if len(sys.argv) == 3:
389 conffile = sys.argv[1]
390 statusfile = sys.argv[2]
391 else:
392 sys.stderr.write("Usage: dhcpdns <dhcpd.conf> <dhcpdns.status>")
393 ipaddrs = parseconfig(conffile)
394 oldips = parsestatus(statusfile)
395 deletedhosts, deletedips = finddeleted(ipaddrs, oldips)
396 print "server localhost"
397 genoutput(ipaddrs, deletedhosts, deletedips)
398 writestatus(statusfile, ipaddrs)
399