|
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 |
|
11 import collections |
|
12 import re |
|
13 import logging; log = logging.getLogger('main') |
|
14 |
|
15 __version__ = '0.1' |
|
16 |
|
17 def parse_options (argv) : |
|
18 """ |
|
19 Parse command-line arguments. |
|
20 """ |
|
21 |
|
22 parser = optparse.OptionParser( |
|
23 prog = argv[0], |
|
24 usage = '%prog: [options]', |
|
25 version = __version__, |
|
26 |
|
27 # module docstring |
|
28 description = __doc__, |
|
29 ) |
|
30 |
|
31 # logging |
|
32 parser.add_option_group(pvl.args.parser(parser)) |
|
33 |
|
34 parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', |
|
35 help="Encoding used for input files") |
|
36 |
|
37 parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', |
|
38 help="Encoding used for output files") |
|
39 |
|
40 # input |
|
41 parser.add_option('--import-zone-hosts', metavar='FILE', |
|
42 help="Load hosts from DNS zone") |
|
43 |
|
44 parser.add_option('--import-dhcp-hosts', metavar='FILE', |
|
45 help="Load hosts from DHCP config") |
|
46 |
|
47 # defaults |
|
48 parser.add_option('--hosts-domain', metavar='DOMAIN', |
|
49 help="Default domain for hosts") |
|
50 |
|
51 parser.add_option('--zone-unused', metavar='HOST', |
|
52 help="DNS name for unallocated hosts") |
|
53 |
|
54 # output |
|
55 parser.add_option('--output-hosts', metavar='FILE', |
|
56 help="Output hosts file") |
|
57 |
|
58 # defaults |
|
59 parser.set_defaults( |
|
60 |
|
61 ) |
|
62 |
|
63 # parse |
|
64 options, args = parser.parse_args(argv[1:]) |
|
65 |
|
66 # apply |
|
67 pvl.args.apply(options, argv[0]) |
|
68 |
|
69 return options, args |
|
70 |
|
71 ZONE_COMMENTS = ( |
|
72 re.compile(r'(?P<owner>[^/]+)\s*-\s+(?P<host>.+)'), |
|
73 re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[/-]\s+(?P<host>.+)'), |
|
74 re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[(]\s*(?P<host>.+)[)]'), |
|
75 re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)'), |
|
76 re.compile(r'(?P<owner>.+)'), |
|
77 ) |
|
78 |
|
79 ZONE_OWNER_MAIL = re.compile(r'(?P<owner>.*?)\s*<(?P<mail>.+?)>') |
|
80 |
|
81 def process_zone_comment (options, hostname, comment) : |
|
82 """ |
|
83 Attempt to parse a host comment field... :D |
|
84 """ |
|
85 |
|
86 yield 'comment', comment |
|
87 |
|
88 for regex in ZONE_COMMENTS : |
|
89 match = regex.match(comment) |
|
90 |
|
91 if match : |
|
92 break |
|
93 else : |
|
94 log.warn("%s: unparsed comment: %s", hostname, comment) |
|
95 return |
|
96 |
|
97 matches = match.groupdict() |
|
98 owner = matches.pop('owner', None) |
|
99 |
|
100 if owner : |
|
101 mail_match = ZONE_OWNER_MAIL.match(owner) |
|
102 |
|
103 if mail_match : |
|
104 mail_matches = mail_match.groupdict() |
|
105 |
|
106 owner = mail_matches['owner'] |
|
107 yield 'comment-mail', mail_matches['mail'] |
|
108 else : |
|
109 mail_matches = { } |
|
110 else : |
|
111 mail_matches = { } |
|
112 |
|
113 yield 'comment-owner', owner |
|
114 |
|
115 for group, value in matches.iteritems() : |
|
116 if value : |
|
117 yield 'comment-{group}'.format(group=group), value.strip() |
|
118 |
|
119 print u"{hostname:20} {comment:80} = {group:15} / {owner:20} <{mail:20}> / {host}".format( |
|
120 hostname = hostname, |
|
121 comment = comment, |
|
122 group = matches.get('group', ''), |
|
123 owner = owner, |
|
124 mail = mail_matches.get('mail', ''), |
|
125 host = matches.get('host', ''), |
|
126 ).encode('utf-8') |
|
127 |
|
128 def process_zone_hosts (options, file) : |
|
129 """ |
|
130 Yield host info from zonefile records. |
|
131 """ |
|
132 |
|
133 for rr in pvl.dns.zone.ZoneRecord.load(file) : |
|
134 if options.zone_unused and rr.name == options.zone_unused : |
|
135 log.debug("%s: skip %s", rr.name, rr) |
|
136 continue |
|
137 |
|
138 elif rr.type == 'A' : |
|
139 ip, = rr.data |
|
140 |
|
141 yield rr.name, 'ip', ip |
|
142 |
|
143 if rr.comment : |
|
144 for field, value in process_zone_comment(options, rr.name, rr.comment) : |
|
145 yield rr.name, field, value |
|
146 |
|
147 elif rr.type == 'CNAME' : |
|
148 host, = rr.data |
|
149 |
|
150 yield host, 'alias', rr.name |
|
151 |
|
152 else : |
|
153 log.warn("%s: unknown rr: %s", rr.name, rr) |
|
154 |
|
155 def process_dhcp_host (options, host, items) : |
|
156 """ |
|
157 Yield host infos from a dhcp host ... { ... } |
|
158 """ |
|
159 |
|
160 hostname = None |
|
161 ethernet = [] |
|
162 fixed_address = None |
|
163 |
|
164 for item in items : |
|
165 item, args = item[0], item[1:] |
|
166 |
|
167 if item == 'hardware' : |
|
168 _ethernet, ethernet = args |
|
169 assert _ethernet == 'ethernet' |
|
170 elif item == 'fixed-address' : |
|
171 fixed_address, = args |
|
172 elif item == 'option' : |
|
173 option = args.pop(0) |
|
174 |
|
175 if option == 'host-name' : |
|
176 hostname, = args |
|
177 else : |
|
178 log.warn("host %s: ignore unknown option: %s", host, option) |
|
179 else : |
|
180 log.warn("host %s: ignore unknown item: %s", host, item) |
|
181 |
|
182 # determine hostname |
|
183 if hostname : |
|
184 pass |
|
185 elif fixed_address and not re.match(r'\d+\.\d+\.\d+.\d+', fixed_address) : |
|
186 hostname, domain = fixed_address.split('.', 1) |
|
187 elif '-' in host : |
|
188 hostname, suffix = host.rsplit('-', 1) |
|
189 else : |
|
190 log.warn("%s: guess hostname: %s", host, host) |
|
191 hostname = host |
|
192 |
|
193 if hostname : |
|
194 yield hostname, 'ethernet', ethernet |
|
195 |
|
196 def process_dhcp_hosts (options, blocks) : |
|
197 """ |
|
198 Process hosts from a parsed block |
|
199 """ |
|
200 |
|
201 for block, items, blocks in blocks : |
|
202 log.info("%s", block) |
|
203 |
|
204 block, args = block[0], block[1:] |
|
205 |
|
206 if block == 'group' : |
|
207 for info in process_dhcp_hosts(options, blocks) : |
|
208 yield info |
|
209 elif block == 'host' : |
|
210 host, = args |
|
211 |
|
212 try : |
|
213 for info in process_dhcp_host(options, host, items) : |
|
214 yield info |
|
215 except ValueError as error : |
|
216 log.warn("%s: invalid host: %s", host, error) |
|
217 else: |
|
218 log.warn("ignore unknown block: %s", block) |
|
219 |
|
220 def process_dhcp_conf (options, file) : |
|
221 items, blocks = pvl.dhcp.config.DHCPConfigParser().load(file) |
|
222 |
|
223 for item in items : |
|
224 item, args = item[0], item[1:] |
|
225 |
|
226 if item == 'include' : |
|
227 include, = args |
|
228 for info in process_dhcp_conf(options, pvl.args.apply_file(include)) : |
|
229 yield info |
|
230 else : |
|
231 log.warn("ignore unknown item: %s", item) |
|
232 |
|
233 for info in process_dhcp_hosts(options, blocks) : |
|
234 yield info |
|
235 |
|
236 def apply_hosts_import (options) : |
|
237 """ |
|
238 Import host infos from given files. |
|
239 """ |
|
240 |
|
241 if options.import_zone_hosts: |
|
242 for info in process_zone_hosts(options, |
|
243 pvl.args.apply_file(options.import_zone_hosts)) : |
|
244 yield info |
|
245 |
|
246 if options.import_dhcp_hosts: |
|
247 for info in process_dhcp_conf(options, |
|
248 pvl.args.apply_file(options.import_dhcp_hosts)) : |
|
249 yield info |
|
250 |
|
251 def process_hosts_import (options, import_hosts) : |
|
252 """ |
|
253 Import host definitions from given infos |
|
254 """ |
|
255 |
|
256 hosts = collections.defaultdict(lambda: collections.defaultdict(list)) |
|
257 |
|
258 for host, field, value in import_hosts : |
|
259 hosts[host][field].append(value) |
|
260 |
|
261 return hosts.iteritems() |
|
262 |
|
263 |
|
264 def main (argv) : |
|
265 options, args = parse_options(argv) |
|
266 |
|
267 if args : |
|
268 # direct from file |
|
269 hosts = pvl.args.apply_files(args, 'r', options.input_charset) |
|
270 else : |
|
271 # import |
|
272 import_hosts = apply_hosts_import(options) |
|
273 hosts = process_hosts_import(options, import_hosts) |
|
274 |
|
275 # output |
|
276 if options.output_hosts : |
|
277 for host, fields in hosts : |
|
278 print host |
|
279 |
|
280 for field, values in fields.iteritems() : |
|
281 for value in values : |
|
282 print "\t", field, "\t", value.encode(options.output_charset) |
|
283 |
|
284 return 0 |
|
285 |
|
286 if __name__ == '__main__': |
|
287 pvl.args.main(main) |