1 #!/usr/bin/env python |
|
2 |
|
3 import pvl.args |
|
4 import pvl.hosts |
|
5 import pvl.dns.zone |
|
6 |
|
7 import ipaddr |
|
8 import logging; log = logging.getLogger('pvl.hosts-dns') |
|
9 import optparse |
|
10 |
|
11 def process_hosts_alias (options, origin, host_domain, alias, host) : |
|
12 """ |
|
13 Resolve alias@domain within given zone. |
|
14 """ |
|
15 |
|
16 if host_domain : |
|
17 alias = pvl.dns.join(alias, host_domain) |
|
18 else : |
|
19 raise ValueError("no domain given for host %s alias %s" % (host, alias, )) |
|
20 |
|
21 if alias.endswith('.' + origin) : |
|
22 # strip |
|
23 alias = alias[:(len(alias) - len(origin) - 1)] |
|
24 else: |
|
25 raise ValueError("alias domain outside of origin: %s / %s" % (alias, origin)) |
|
26 |
|
27 return pvl.dns.zone.ZoneRecord.CNAME(alias, host) |
|
28 |
|
29 def process_hosts_names (options, hosts, origin) : |
|
30 """ |
|
31 Yield ZoneRecords for hosts within the given zone. |
|
32 """ |
|
33 |
|
34 for host in hosts : |
|
35 # determine label within zone |
|
36 if not origin : |
|
37 label = pvl.dns.join(host.name, host.domain) |
|
38 elif host.domain == origin : |
|
39 label = host.name |
|
40 elif host.domain and host.domain.endswith('.' + origin) : |
|
41 fqdn = pvl.dns.join(host.name, host.domain) |
|
42 label = fqdn[:(len(fqdn) - len(origin) - 1)] |
|
43 elif host.domain : |
|
44 log.debug("%s: domain out of zone: %s", host, origin) |
|
45 continue |
|
46 else : |
|
47 log.debug("%s: fqdn out of zone: %s", host, origin) |
|
48 continue |
|
49 |
|
50 if host.forward is None : |
|
51 pass |
|
52 elif host.forward : |
|
53 forward = pvl.dns.zone.fqdn(host.forward) |
|
54 |
|
55 log.info("%s: forward: %s", host, forward) |
|
56 |
|
57 yield pvl.dns.zone.ZoneRecord.CNAME(label, forward) |
|
58 continue |
|
59 else : |
|
60 log.info("%s: skip forward", host) |
|
61 continue |
|
62 |
|
63 if host.ip : |
|
64 yield pvl.dns.zone.ZoneRecord.A(label, host.ip) |
|
65 |
|
66 if host.alias4 : |
|
67 yield pvl.dns.zone.ZoneRecord.A(host.ALIAS4_FMT.format(host=label), host.ip) |
|
68 |
|
69 if host.ip6 : |
|
70 yield pvl.dns.zone.ZoneRecord.AAAA(label, host.ip6) |
|
71 |
|
72 if host.alias6 : |
|
73 yield pvl.dns.zone.ZoneRecord.AAAA(label.ALIAS6_FMT.format(host=host), host.ip6) |
|
74 |
|
75 if host.location and host.location_domain: |
|
76 yield process_hosts_alias(options, origin, host.location_domain, host.location, label) |
|
77 elif host.location: |
|
78 yield process_hosts_alias(options, origin, host.domain, host.location, label) |
|
79 |
|
80 for alias in host.alias : |
|
81 yield process_hosts_alias(options, origin, host.domain, alias, label) |
|
82 |
|
83 for alias4 in host.alias4 : |
|
84 yield process_hosts_alias(options, origin, host.domain, alias4, host.ALIAS4_FMT.format(host=label)) |
|
85 |
|
86 for alias6 in host.alias6 : |
|
87 yield process_hosts_alias(options, origin, host.domain, alias6, host.ALIAS6_FMT.format(host=label)) |
|
88 |
|
89 def process_hosts_forward (options, hosts, origin) : |
|
90 """ |
|
91 Generate DNS ZoneRecords for for hosts within the given zone origin. |
|
92 """ |
|
93 |
|
94 if options.add_origin : |
|
95 yield pvl.dns.zone.ZoneDirective.build(None, 'ORIGIN', origin) |
|
96 |
|
97 by_name = dict() |
|
98 by_name_type = dict() |
|
99 |
|
100 # list of types thare are allowed to be present for a host |
|
101 MULTI_TYPES = ('A', 'AAAA') |
|
102 |
|
103 for rr in process_hosts_names(options, hosts, origin) : |
|
104 if (rr.name, rr.type) in by_name_type : |
|
105 raise ValueError("%s: duplicate name/type: %s: %s" % (rr.name, rr, by_name_type[(rr.name, rr.type)])) |
|
106 elif rr.type in MULTI_TYPES : |
|
107 by_name_type[(rr.name, rr.type)] = rr |
|
108 elif rr.name in by_name : |
|
109 raise ValueError("%s: duplicate name: %s: %s" % (rr.name, rr, by_name[rr.name])) |
|
110 |
|
111 # always check these |
|
112 by_name[rr.name] = rr |
|
113 |
|
114 # preserve ordering |
|
115 yield rr |
|
116 |
|
117 def split_ipv6_parts (prefix) : |
|
118 for hextet in prefix.rstrip(':').split(':') : |
|
119 for nibble in hextet.rjust(4, '0') : |
|
120 yield nibble |
|
121 |
|
122 def build_ipv6_parts (parts) : |
|
123 for i in xrange(0, len(parts), 4) : |
|
124 yield ''.join(parts[i:i+4]) |
|
125 |
|
126 # suffix :: |
|
127 if len(parts) < 32 : |
|
128 yield '' |
|
129 yield '' |
|
130 |
|
131 def parse_prefix (prefix) : |
|
132 """ |
|
133 Return an ipaddr.IPNetwork from given IPv4/IPv6 prefix. |
|
134 |
|
135 >>> parse_prefix('127.0.0.0/8') |
|
136 IPv4Network('127.0.0.0/8') |
|
137 >>> parse_prefix('192.0.2.128/26') |
|
138 IPv4Network('192.0.2.128/26') |
|
139 >>> parse_prefix('192.0.2.128-26') |
|
140 IPv4Network('192.0.2.128-26') |
|
141 >>> parse_prefix('127.') |
|
142 IPv4Network('127.0.0.0/8') |
|
143 >>> parse_prefix('10') |
|
144 IPv4Network('10.0.0.0/8') |
|
145 >>> parse_prefix('192.168') |
|
146 IPv4Network('192.168.0.0/16') |
|
147 >>> parse_prefix('fe80:') |
|
148 IPv6Network('fe80::/16') |
|
149 >>> parse_prefix('2001:db8::') |
|
150 IPv6Network('2001:db8::/32') |
|
151 >>> parse_prefix('2001:db8:1:2') |
|
152 IPv6Network('2001:db8:1:2::/64') |
|
153 """ |
|
154 |
|
155 if '/' in prefix : |
|
156 return ipaddr.IPNetwork(prefix) |
|
157 |
|
158 elif '-' in prefix : |
|
159 return ipaddr.IPNetwork(prefix.replace('-', '/')) |
|
160 |
|
161 elif '.' in prefix or prefix.isdigit() : |
|
162 parts = prefix.rstrip('.').split('.') |
|
163 prefixlen = len(parts) * 8 |
|
164 |
|
165 return ipaddr.IPv4Network('{prefix}/{prefixlen}'.format( |
|
166 prefix = '.'.join(parts + ['0' for i in xrange(4 - len(parts))]), |
|
167 prefixlen = prefixlen, |
|
168 )) |
|
169 |
|
170 elif ':' in prefix : |
|
171 parts = list(split_ipv6_parts(prefix)) |
|
172 prefixlen = len(parts) * 4 |
|
173 |
|
174 return ipaddr.IPv6Network('{prefix}/{prefixlen}'.format( |
|
175 prefix = ':'.join(build_ipv6_parts(parts)), |
|
176 prefixlen = prefixlen, |
|
177 )) |
|
178 |
|
179 else : |
|
180 raise ValueError("Unrecognized IP prefix string: %s" % (prefix, )) |
|
181 |
|
182 def process_hosts_ips (options, hosts, prefix) : |
|
183 """ |
|
184 Yield (ip, fqnd) for hosts within given prefix. |
|
185 """ |
|
186 |
|
187 for host in hosts : |
|
188 if prefix.version == 4 : |
|
189 ip = host.ip |
|
190 elif prefix.version == 6 : |
|
191 ip = host.ip6 |
|
192 else : |
|
193 raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version)) |
|
194 |
|
195 if not ip : |
|
196 log.debug("%s: no ip%d", host, prefix.version) |
|
197 continue |
|
198 |
|
199 if ip not in prefix : |
|
200 log.debug("%s: %s out of prefix: %s", host, ip, prefix) |
|
201 continue |
|
202 |
|
203 label = pvl.dns.zone.reverse_label(prefix, ip) |
|
204 |
|
205 if host.reverse is None : |
|
206 fqdn = host.fqdn() |
|
207 |
|
208 log.info("%s %s[%s]: PTR %s", host, prefix, ip, fqdn) |
|
209 |
|
210 yield host, ip, pvl.dns.zone.ZoneRecord.PTR(label, fqdn) |
|
211 |
|
212 elif host.reverse : |
|
213 alias = pvl.dns.zone.fqdn(host.reverse) |
|
214 |
|
215 log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias) |
|
216 |
|
217 yield host, ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias) |
|
218 |
|
219 else : |
|
220 log.info("%s %s[%s]: omit", host, prefix, ip) |
|
221 continue |
|
222 |
|
223 |
|
224 def process_hosts_reverse (options, hosts, prefix) : |
|
225 """ |
|
226 Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts. |
|
227 """ |
|
228 |
|
229 # collect data for records |
|
230 by_ip = dict() |
|
231 for host, ip, rr in process_hosts_ips(options, hosts, prefix) : |
|
232 if ip in by_ip : |
|
233 raise ValueError("%s: duplicate ip: %s: %s" % (host, ip, by_ip[ip])) |
|
234 else : |
|
235 by_ip[ip] = rr |
|
236 |
|
237 if options.unknown_host : |
|
238 # enumerate all of them |
|
239 iter_ips = prefix.iterhosts() |
|
240 else : |
|
241 iter_ips = sorted(by_ip) |
|
242 |
|
243 for ip in iter_ips : |
|
244 if ip in by_ip : |
|
245 yield by_ip[ip] |
|
246 elif options.unknown_host : |
|
247 label = pvl.dns.zone.reverse_label(prefix, ip) |
|
248 fqdn = pvl.dns.zone.fqdn(options.unknown_host, options.hosts_domain) |
|
249 |
|
250 log.info("%s %s[%s]: unused PTR %s", options.unknown_host, ip, prefix, fqdn) |
|
251 |
|
252 yield pvl.dns.zone.ZoneRecord.PTR(label, fqdn) |
|
253 else : |
|
254 continue |
|
255 |
|
256 def apply_zone (options, zone) : |
|
257 """ |
|
258 Output given ZoneRecord's |
|
259 """ |
|
260 |
|
261 for record in zone : |
|
262 print unicode(record) |
|
263 |
|
264 def main (argv) : |
|
265 """ |
|
266 Generate bind zonefiles from host definitions. |
|
267 """ |
|
268 |
|
269 parser = optparse.OptionParser(main.__doc__) |
|
270 parser.add_option_group(pvl.args.parser(parser)) |
|
271 parser.add_option_group(pvl.hosts.optparser(parser)) |
|
272 |
|
273 parser.add_option('--add-origin', action='store_true', |
|
274 help="Include $ORIGIN directive in zone") |
|
275 |
|
276 parser.add_option('--forward-zone', metavar='DOMAIN', |
|
277 help="Generate forward zone for domain") |
|
278 |
|
279 parser.add_option('--reverse-zone', metavar='PREFIX', |
|
280 help="Generate reverse zone for prefix") |
|
281 |
|
282 parser.add_option('--unknown-host', metavar='NAME', |
|
283 help="Generate records for unused IPs") |
|
284 |
|
285 options, args = parser.parse_args(argv[1:]) |
|
286 pvl.args.apply(options) |
|
287 |
|
288 # input |
|
289 hosts = pvl.hosts.apply(options, args) |
|
290 |
|
291 # process |
|
292 if options.forward_zone : |
|
293 apply_zone(options, |
|
294 process_hosts_forward(options, hosts, options.forward_zone), |
|
295 ) |
|
296 |
|
297 if options.reverse_zone : |
|
298 apply_zone(options, |
|
299 process_hosts_reverse(options, hosts, parse_prefix(options.reverse_zone)), |
|
300 ) |
|
301 |
|
302 if __name__ == '__main__': |
|
303 pvl.args.main(main) |
|