1 #!/usr/bin/env python |
|
2 |
|
3 """ |
|
4 Manipulate host definitions for dns/dhcp. |
|
5 """ |
|
6 |
|
7 import pvl.args, optparse |
|
8 import pvl.dns.zone |
|
9 import pvl.dhcp.config |
|
10 import pvl.ldap.args |
|
11 |
|
12 import ipaddr |
|
13 import optparse |
|
14 import collections |
|
15 import re |
|
16 import logging; log = logging.getLogger('main') |
|
17 |
|
18 __version__ = '0.1' |
|
19 |
|
20 def parse_options (argv) : |
|
21 """ |
|
22 Parse command-line arguments. |
|
23 """ |
|
24 |
|
25 parser = optparse.OptionParser( |
|
26 prog = argv[0], |
|
27 usage = '%prog: [options]', |
|
28 version = __version__, |
|
29 |
|
30 # module docstring |
|
31 description = __doc__, |
|
32 ) |
|
33 |
|
34 # logging |
|
35 parser.add_option_group(pvl.args.parser(parser)) |
|
36 parser.add_option_group(pvl.ldap.args.parser(parser)) |
|
37 |
|
38 parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', |
|
39 help="Encoding used for input files") |
|
40 |
|
41 parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', |
|
42 help="Encoding used for output files") |
|
43 |
|
44 # input |
|
45 parser.add_option('--import-zone-hosts', metavar='FILE', |
|
46 help="Load hosts from DNS zone") |
|
47 |
|
48 parser.add_option('--import-dhcp-hosts', metavar='FILE', |
|
49 help="Load hosts from DHCP config") |
|
50 |
|
51 parser.add_option('--dump-host-comments', action='store_true', |
|
52 help="Dump out info on imported host comments") |
|
53 |
|
54 # defaults |
|
55 parser.add_option('--hosts-domain', metavar='DOMAIN', |
|
56 help="Default domain for hosts") |
|
57 |
|
58 parser.add_option('--zone-unused', metavar='HOST', |
|
59 help="DNS name for unallocated hosts") |
|
60 |
|
61 # output |
|
62 parser.add_option('--output-hosts', metavar='FILE', |
|
63 help="Output hosts file") |
|
64 |
|
65 parser.add_option('--output-prefix', metavar='PREFIX', |
|
66 help="Select hosts by ip prefix") |
|
67 |
|
68 # defaults |
|
69 parser.set_defaults( |
|
70 |
|
71 ) |
|
72 |
|
73 # parse |
|
74 options, args = parser.parse_args(argv[1:]) |
|
75 |
|
76 # apply |
|
77 pvl.args.apply(options, argv[0]) |
|
78 |
|
79 return options, args |
|
80 |
|
81 def import_zone_hosts (options, file) : |
|
82 """ |
|
83 Yield host info from zonefile records. |
|
84 """ |
|
85 |
|
86 for rr in pvl.dns.zone.ZoneRecord.load(file) : |
|
87 if options.zone_unused and rr.name == options.zone_unused : |
|
88 log.debug("%s: skip %s", rr.name, rr) |
|
89 continue |
|
90 |
|
91 elif rr.type == 'A' : |
|
92 ip, = rr.data |
|
93 |
|
94 yield rr.name, 'ip', ipaddr.IPAddress(ip) |
|
95 |
|
96 if rr.comment : |
|
97 yield rr.name, 'comment', rr.comment |
|
98 |
|
99 elif rr.type == 'CNAME' : |
|
100 host, = rr.data |
|
101 |
|
102 yield host, 'alias', rr.name |
|
103 |
|
104 else : |
|
105 log.warn("%s: unknown rr: %s", rr.name, rr) |
|
106 |
|
107 def import_dhcp_host (options, host, items) : |
|
108 """ |
|
109 Yield host infos from a dhcp host ... { ... } |
|
110 """ |
|
111 |
|
112 hostname = None |
|
113 ethernet = [] |
|
114 fixed_address = None |
|
115 |
|
116 for item in items : |
|
117 item, args = item[0], item[1:] |
|
118 |
|
119 if item == 'hardware' : |
|
120 _ethernet, ethernet = args |
|
121 assert _ethernet == 'ethernet' |
|
122 elif item == 'fixed-address' : |
|
123 fixed_address, = args |
|
124 elif item == 'option' : |
|
125 option = args.pop(0) |
|
126 |
|
127 if option == 'host-name' : |
|
128 hostname, = args |
|
129 else : |
|
130 log.warn("host %s: ignore unknown option: %s", host, option) |
|
131 else : |
|
132 log.warn("host %s: ignore unknown item: %s", host, item) |
|
133 |
|
134 # determine hostname |
|
135 suffix = None |
|
136 |
|
137 if '-' in host : |
|
138 hostname, suffix = host.rsplit('-', 1) |
|
139 else : |
|
140 hostname = host |
|
141 |
|
142 if fixed_address and not re.match(r'\d+\.\d+\.\d+.\d+', fixed_address) : |
|
143 hostname, domain = fixed_address.split('.', 1) |
|
144 |
|
145 #if suffix : |
|
146 # yield hostname, 'ethernet:{suffix}'.format(suffix=suffix), ethernet |
|
147 if hostname and ethernet : |
|
148 yield hostname, 'ethernet', ethernet |
|
149 else : |
|
150 log.warn("%s: no hostname/ethernet: %s/%s", host, hostname, ethernet) |
|
151 |
|
152 def import_dhcp_hosts (options, blocks) : |
|
153 """ |
|
154 Process hosts from a parsed block |
|
155 """ |
|
156 |
|
157 for block, items, blocks in blocks : |
|
158 log.info("%s", block) |
|
159 |
|
160 block, args = block[0], block[1:] |
|
161 |
|
162 if block == 'group' : |
|
163 for info in import_dhcp_hosts(options, blocks) : |
|
164 yield info |
|
165 elif block == 'host' : |
|
166 host, = args |
|
167 |
|
168 try : |
|
169 for info in import_dhcp_host(options, host, items) : |
|
170 yield info |
|
171 except ValueError as error : |
|
172 log.warn("%s: invalid host: %s", host, error) |
|
173 else: |
|
174 log.warn("ignore unknown block: %s", block) |
|
175 |
|
176 def import_dhcp_conf (options, file) : |
|
177 items, blocks = pvl.dhcp.config.DHCPConfigParser().load(file) |
|
178 |
|
179 for item in items : |
|
180 item, args = item[0], item[1:] |
|
181 |
|
182 if item == 'include' : |
|
183 include, = args |
|
184 for info in import_dhcp_conf(options, pvl.args.apply_file(include)) : |
|
185 yield info |
|
186 else : |
|
187 log.warn("ignore unknown item: %s", item) |
|
188 |
|
189 for info in import_dhcp_hosts(options, blocks) : |
|
190 yield info |
|
191 |
|
192 ZONE_COMMENTS = ( |
|
193 re.compile(r'(?P<owner>[^/]+)\s*-\s+(?P<host>.+)'), |
|
194 re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[/-]\s+(?P<host>.+)'), |
|
195 re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[(]\s*(?P<host>.+)[)]'), |
|
196 re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)'), |
|
197 re.compile(r'(?P<owner>.+)'), |
|
198 ) |
|
199 |
|
200 ZONE_OWNER_MAIL = re.compile(r'(?P<owner>.*?)\s*<(?P<mail>.+?)>') |
|
201 |
|
202 def process_zone_comment (options, hostname, comment) : |
|
203 """ |
|
204 Attempt to parse a host comment field... :D |
|
205 |
|
206 Yields (field, value) bits |
|
207 """ |
|
208 |
|
209 for regex in ZONE_COMMENTS : |
|
210 match = regex.match(comment) |
|
211 |
|
212 if match : |
|
213 break |
|
214 else : |
|
215 log.warn("%s: unparsed comment: %s", hostname, comment) |
|
216 return |
|
217 |
|
218 matches = match.groupdict() |
|
219 owner = matches.pop('owner', None) |
|
220 |
|
221 if owner : |
|
222 mail_match = ZONE_OWNER_MAIL.match(owner) |
|
223 |
|
224 if mail_match : |
|
225 mail_matches = mail_match.groupdict() |
|
226 |
|
227 owner = mail_matches['owner'] |
|
228 yield 'mail', mail_matches['mail'].strip() |
|
229 |
|
230 yield 'owner', owner.strip() |
|
231 |
|
232 for field, value in matches.iteritems() : |
|
233 if value : |
|
234 yield field, value.strip() |
|
235 |
|
236 NONE_OWNERS = set(( |
|
237 u'tech', |
|
238 u'atk', |
|
239 u'toimisto', |
|
240 )) |
|
241 |
|
242 def process_host_owner_ldap (options, host, info) : |
|
243 """ |
|
244 Yield guesses for user from LDAP. |
|
245 """ |
|
246 |
|
247 if info.get('mail') : |
|
248 for user in options.ldap.users.filter( |
|
249 { 'mailLocalAddress': info['mail'] }, |
|
250 { 'uid': info['mail'] }, |
|
251 ) : |
|
252 yield user, None |
|
253 |
|
254 if info.get('group') and info.get('owner') : |
|
255 groups = options.ldap.groups.filter(cn=info['group']) |
|
256 |
|
257 for group in groups : |
|
258 for user in options.ldap.users.filter({ |
|
259 'gidNumber': group['gidNumber'], |
|
260 'cn': info['owner'], |
|
261 }) : |
|
262 yield user, group |
|
263 |
|
264 if info.get('owner') : |
|
265 for user in options.ldap.users.filter({ |
|
266 'cn': info['owner'], |
|
267 }) : |
|
268 yield user, None |
|
269 |
|
270 def process_host_owner (options, host, info) : |
|
271 """ |
|
272 Return (owner, comment) for host based on info, or None. |
|
273 """ |
|
274 |
|
275 if info.get('owner').lower() in NONE_OWNERS : |
|
276 return False |
|
277 |
|
278 # from ldap? |
|
279 for ldap in process_host_owner_ldap(options, host, info) : |
|
280 user, group = ldap |
|
281 |
|
282 if not group : |
|
283 # get group from ldap |
|
284 group = options.ldap.users.group(user) |
|
285 |
|
286 return user['uid'], u"{group} / {user}".format( |
|
287 user = user.getunicode('cn'), |
|
288 group = group.getunicode('cn'), |
|
289 ) |
|
290 |
|
291 def process_host_comments (options, host, info) : |
|
292 """ |
|
293 Process host fields from comment. |
|
294 |
|
295 Attempts to find owner from LDAP.. |
|
296 """ |
|
297 |
|
298 log.debug("%s: %s", host, info) |
|
299 |
|
300 owner = process_host_owner(options, host, info) |
|
301 |
|
302 if owner is False : |
|
303 # do not mark any owner |
|
304 pass |
|
305 |
|
306 elif owner : |
|
307 owner, comment = owner |
|
308 |
|
309 log.info("%s: %s (%s)", host, owner, comment) |
|
310 |
|
311 yield 'owner-comment', comment |
|
312 yield 'owner', owner, |
|
313 |
|
314 else : |
|
315 log.warn("%s: unknown owner: %s", host, info) |
|
316 yield 'comment', "owner: {group} / {owner}".format( |
|
317 group = info.get('group', ''), |
|
318 owner = info.get('owner', ''), |
|
319 ) |
|
320 |
|
321 if info.get('host') : |
|
322 yield 'comment', info['host'] |
|
323 |
|
324 def process_hosts_comments (options, import_hosts) : |
|
325 """ |
|
326 Parse out comments from host imports.. |
|
327 """ |
|
328 |
|
329 for host, field, value in import_hosts : |
|
330 if field != 'comment': |
|
331 yield host, field, value |
|
332 continue |
|
333 |
|
334 fields = dict(process_zone_comment(options, host, value)) |
|
335 |
|
336 if options.dump_host_comments : |
|
337 print u"{host:20} {comment:80} = {group:15} / {owner:20} <{mail:20}> / {hostinfo}".format( |
|
338 host = host, |
|
339 comment = value, |
|
340 group = fields.get('group', ''), |
|
341 owner = fields.get('owner', ''), |
|
342 mail = fields.get('mail', ''), |
|
343 hostinfo = fields.get('host', ''), |
|
344 ).encode('utf-8') |
|
345 |
|
346 |
|
347 for field, value in process_host_comments(options, host, fields) : |
|
348 yield host, field, value |
|
349 |
|
350 def apply_hosts_import (options) : |
|
351 """ |
|
352 Import host infos from given files. |
|
353 """ |
|
354 |
|
355 if options.import_zone_hosts: |
|
356 for info in import_zone_hosts(options, |
|
357 pvl.args.apply_file(options.import_zone_hosts)) : |
|
358 yield info |
|
359 |
|
360 if options.import_dhcp_hosts: |
|
361 for info in import_dhcp_conf(options, |
|
362 pvl.args.apply_file(options.import_dhcp_hosts)) : |
|
363 yield info |
|
364 |
|
365 def import_hosts (options) : |
|
366 """ |
|
367 Import hosts from dns/dhcp. |
|
368 """ |
|
369 |
|
370 import_hosts = apply_hosts_import(options) |
|
371 import_hosts = process_hosts_comments(options, import_hosts) |
|
372 |
|
373 # gather |
|
374 hosts = collections.defaultdict(lambda: collections.defaultdict(list)) |
|
375 |
|
376 for host, field, value in import_hosts : |
|
377 hosts[host][field].append(value) |
|
378 |
|
379 return hosts.iteritems() |
|
380 |
|
381 def process_export_hosts (options, hosts) : |
|
382 if options.output_prefix : |
|
383 prefix = ipaddr.IPNetwork(options.output_prefix) |
|
384 else : |
|
385 prefix = None |
|
386 |
|
387 for host, fields in hosts : |
|
388 ip = fields.get('ip') |
|
389 |
|
390 # sort by IP |
|
391 if ip : |
|
392 sort = ip = ip[0] |
|
393 else : |
|
394 # fake, to sort correctly |
|
395 sort = ipaddr.IPAddress(0) |
|
396 |
|
397 # select |
|
398 if prefix: |
|
399 if not (ip and ip in prefix) : |
|
400 continue |
|
401 |
|
402 yield sort, host, fields |
|
403 |
|
404 def export_hosts (options, hosts) : |
|
405 """ |
|
406 Export hosts to file. |
|
407 """ |
|
408 |
|
409 file = pvl.args.apply_file(options.output_hosts, 'w', options.output_charset) |
|
410 |
|
411 # filter + sort |
|
412 hosts = [(host, fields) for sort, host, fields in sorted(process_export_hosts(options, hosts))] |
|
413 |
|
414 for host, fields in hosts : |
|
415 for comment in fields.get('comment', ()) : |
|
416 print >>file, u"# {comment}".format(comment=comment) |
|
417 |
|
418 print >>file, u"[{host}]".format(host=host) |
|
419 |
|
420 for field, fmt in ( |
|
421 ('ip', None), |
|
422 ('ethernet', None), |
|
423 ('owner', u"\t{field:15} = {value} # {fields[owner-comment][0]}"), |
|
424 ) : |
|
425 if not fmt : |
|
426 fmt = u"\t{field:15} = {value}" |
|
427 |
|
428 for value in fields.get(field, ()) : |
|
429 print >>file, fmt.format(field=field, value=value, fields=fields) |
|
430 |
|
431 print >>file |
|
432 |
|
433 def main (argv) : |
|
434 options, args = parse_options(argv) |
|
435 |
|
436 options.ldap = pvl.ldap.args.apply(options) |
|
437 |
|
438 if args : |
|
439 # direct from file |
|
440 hosts = pvl.args.apply_files(args, 'r', options.input_charset) |
|
441 else : |
|
442 # import |
|
443 hosts = import_hosts(options) |
|
444 |
|
445 # output |
|
446 if options.output_hosts : |
|
447 export_hosts(options, hosts) |
|
448 |
|
449 if __name__ == '__main__': |
|
450 pvl.args.main(main) |
|