|
1 #!/usr/bin/env python |
|
2 |
|
3 """ |
|
4 Go through a dhcp conf file looking for fixed-address stanzas, and make sure that they are valid. |
|
5 """ |
|
6 |
|
7 __version__ = '0.0.1-dev' |
|
8 |
|
9 import optparse |
|
10 import codecs |
|
11 import logging |
|
12 |
|
13 import socket |
|
14 |
|
15 log = logging.getLogger('main') |
|
16 |
|
17 # command-line options, global state |
|
18 options = None |
|
19 |
|
20 def parse_options (argv) : |
|
21 """ |
|
22 Parse command-line arguments. |
|
23 """ |
|
24 |
|
25 prog = argv[0] |
|
26 |
|
27 parser = optparse.OptionParser( |
|
28 prog = prog, |
|
29 usage = '%prog: [options]', |
|
30 version = __version__, |
|
31 |
|
32 # module docstring |
|
33 description = __doc__, |
|
34 ) |
|
35 |
|
36 # logging |
|
37 general = optparse.OptionGroup(parser, "General Options") |
|
38 |
|
39 general.add_option('-q', '--quiet', dest='loglevel', action='store_const', const=logging.ERROR, help="Less output") |
|
40 general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") |
|
41 general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") |
|
42 |
|
43 parser.add_option_group(general) |
|
44 |
|
45 # input/output |
|
46 parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', |
|
47 help="Encoding used for input files") |
|
48 |
|
49 # |
|
50 parser.add_option('--doctest', action='store_true', |
|
51 help="Run module doctests") |
|
52 |
|
53 # defaults |
|
54 parser.set_defaults( |
|
55 loglevel = logging.WARN, |
|
56 ) |
|
57 |
|
58 # parse |
|
59 options, args = parser.parse_args(argv[1:]) |
|
60 |
|
61 # configure |
|
62 logging.basicConfig( |
|
63 format = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s', |
|
64 level = options.loglevel, |
|
65 ) |
|
66 |
|
67 return options, args |
|
68 |
|
69 def parse_fixedaddrs (file) : |
|
70 """ |
|
71 Go through lines in given .conf file, looking for fixed-address stanzas. |
|
72 """ |
|
73 |
|
74 filename = file.name |
|
75 |
|
76 for lineno, line in enumerate(file) : |
|
77 # comments? |
|
78 if '#' in line : |
|
79 line, comment = line.split('#', 1) |
|
80 |
|
81 else : |
|
82 comment = None |
|
83 |
|
84 # whitespace |
|
85 line = line.strip() |
|
86 |
|
87 if not line : |
|
88 # empty |
|
89 continue |
|
90 |
|
91 # grep |
|
92 if 'fixed-address' in line : |
|
93 # great parsing :) |
|
94 fixedaddr = line.replace('fixed-address', '').replace(';', '').strip() |
|
95 |
|
96 log.debug("%s:%d: %s: %s", filename, lineno, fixedaddr, line) |
|
97 |
|
98 yield lineno, fixedaddr |
|
99 |
|
100 def resolve_addr (addr, af=socket.AF_INET, socktype=socket.SOCK_STREAM) : |
|
101 """ |
|
102 Resolve given address for given AF_INET, returning a list of resolved addresses. |
|
103 |
|
104 Raises an Exception if failed. |
|
105 |
|
106 >>> resolve_addr('127.0.0.1') |
|
107 ['127.0.0.1'] |
|
108 """ |
|
109 |
|
110 if not addr : |
|
111 raise Exception("Empty addr: %r", addr) |
|
112 |
|
113 # resolve |
|
114 result = socket.getaddrinfo(addr, None, af, socktype) |
|
115 |
|
116 #log.debug("%s: %s", addr, result) |
|
117 |
|
118 # addresses |
|
119 addrs = list(sorted(set(sockaddr[0] for family, socktype, proto, canonname, sockaddr in result))) |
|
120 |
|
121 return addrs |
|
122 |
|
123 def check_file_hosts (file) : |
|
124 """ |
|
125 Check all fixed-address parameters in given file. |
|
126 """ |
|
127 |
|
128 filename = file.name |
|
129 fail = 0 |
|
130 |
|
131 for lineno, addr in parse_fixedaddrs(file) : |
|
132 # lookup |
|
133 try : |
|
134 resolved = resolve_addr(addr) |
|
135 |
|
136 except Exception as e: |
|
137 log.warning("%s:%d: failed to resolve: %s: %s", filename, lineno, addr, e) |
|
138 fail += 1 |
|
139 |
|
140 else : |
|
141 log.debug("%s:%d: %s: %r", filename, lineno, addr, resolved) |
|
142 |
|
143 return fail |
|
144 |
|
145 def open_file (path, mode, charset) : |
|
146 """ |
|
147 Open unicode-enabled file from path, with - using stdio. |
|
148 """ |
|
149 |
|
150 if path == '-' : |
|
151 # use stdin/out based on mode |
|
152 stream, func = { |
|
153 'r': (sys.stdin, codecs.getreader), |
|
154 'w': (sys.stdout, codecs.getwriter), |
|
155 }[mode[0]] |
|
156 |
|
157 # wrap |
|
158 return func(charset)(stream) |
|
159 |
|
160 else : |
|
161 # open |
|
162 return codecs.open(path, mode, charset) |
|
163 |
|
164 def main (argv) : |
|
165 global options |
|
166 |
|
167 options, args = parse_options(argv) |
|
168 |
|
169 if options.doctest : |
|
170 import doctest |
|
171 fail, total = doctest.testmod() |
|
172 return fail |
|
173 |
|
174 if args : |
|
175 # open files |
|
176 input_files = [open_file(path, 'r', options.input_charset) for path in args] |
|
177 |
|
178 else : |
|
179 # default to stdout |
|
180 input_files = [open_file('-', 'r', options.input_charset)] |
|
181 |
|
182 # process zone data |
|
183 for file in input_files : |
|
184 log.info("Reading zone: %s", file) |
|
185 |
|
186 fail = check_file_hosts(file) |
|
187 |
|
188 if fail : |
|
189 log.warn("DHCP hosts check failed: %d", fail) |
|
190 return 2 |
|
191 |
|
192 else : |
|
193 log.info("DHCP hosts check OK") |
|
194 |
|
195 return 0 |
|
196 |
|
197 if __name__ == '__main__': |
|
198 import sys |
|
199 |
|
200 sys.exit(main(sys.argv)) |
|
201 |