DHCPD config to DNS update script

This one takes some explaining. Here is the scenario in which it's used:

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: apply_changes.sh

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 
dhcpdns.py