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 = "in.barrelfish.org"
23 REVZONE = "110.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 IP_RE = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
296
297 def parseconfig(filename):
298 dc = dhcpdconf(filename)
299 ipaddrs = {}
300
301 def addhost(host):
302 if host.has_key('fixed-address') and IP_RE.match(host['fixed-address']):
303 if host.has_key('ddns-hostname'):
304 name = host['ddns-hostname']
305 else:
306 name = host['name']
307 ipaddrs[name] = host['fixed-address']
308
309 for n in range(len(dc.groups)):
310 group = dc.groups[n]
311 for m in range(len(group['hosts'])):
312 addhost(group['hosts'][m])
313 for n in range(len(dc.subnets)):
314 subnet = dc.subnets[n]
315 for m in range(len(subnet['hosts'])):
316 addhost(subnet['hosts'][m])
317 for n in range(len(dc.hosts)):
318 addhost(dc.hosts[n])
319
320 return ipaddrs
321
322
323 def parsestatus(filename):
324 ipaddrs = {}
325 fh = open(filename, 'r')
326 for line in fh:
327 data = line.split()
328 if len(data) != 2:
329 sys.stderr.write("Error: failed to parse %s\n", filename)
330 sys.exit(1)
331 ipaddrs[data[0]] = data[1]
332 fh.close()
333 return ipaddrs
334
335 def writestatus(filename, ipaddrs):
336 fh = open(filename, 'w')
337 keys = ipaddrs.keys()
338 keys.sort()
339 for host in keys:
340 fh.write("%s\t%s\n" % (host, ipaddrs[host]))
341 fh.close()
342
343
344 def finddeleted(ipaddrs, oldips):
345 delhosts = []
346 delips = []
347 for (host, ip) in oldips.items():
348 if host not in ipaddrs.keys():
349 delhosts.append(host)
350 if ip not in ipaddrs.values():
351 delips.append(ip)
352 return delhosts, delips
353
354
355 def makearpa(ip):
356 octets = ip.split('.')
357 assert(len(octets) == 4)
358 octets.reverse()
359 return ("%s.%s.%s.%s.in-addr.arpa" % tuple(octets))
360
361
362 def genoutput(ipaddrs, deletedhosts, deletedips):
363 print "zone %s" % FWDZONE
364 deletedhosts.sort()
365 for host in deletedhosts:
366 print "update delete %s.%s." % (host, FWDZONE)
367 keys = ipaddrs.keys()
368 keys.sort()
369 for host in keys:
370 print "update delete %s.%s." % (host, FWDZONE)
371 print "update add %s.%s. %s A %s" % (host, FWDZONE, TTL, ipaddrs[host])
372
373 # reverse the ipaddrs hash
374 hostnames = {}
375 for (host, ip) in ipaddrs.items():
376 hostnames[ip] = host
377
378 print "send"
379 print "zone %s" % REVZONE
380 deletedips.sort()
381 for ip in deletedips:
382 print "update delete %s." % makearpa(ip)
383 keys = hostnames.keys()
384 keys.sort()
385 for ip in keys:
386 print "update delete %s." % makearpa(ip)
387 print "update add %s. %s PTR %s.%s." % (makearpa(ip), TTL, hostnames[ip], FWDZONE)
388 print "send"
389
390 if __name__ == "__main__":
391 # parse the config file and extract hostnames and IPs
392 if len(sys.argv) == 3:
393 conffile = sys.argv[1]
394 statusfile = sys.argv[2]
395 else:
396 sys.stderr.write("Usage: dhcpdns <dhcpd.conf> <dhcpdns.status>")
397 ipaddrs = parseconfig(conffile)
398 oldips = parsestatus(statusfile)
399 deletedhosts, deletedips = finddeleted(ipaddrs, oldips)
400 print "server localhost"
401 genoutput(ipaddrs, deletedhosts, deletedips)
402 writestatus(statusfile, ipaddrs)
403