|
1 """ |
|
2 Generate zonefile records from hosts |
|
3 """ |
|
4 |
|
5 import ipaddr |
|
6 import logging; log = logging.getLogger('pvl.hosts.zone') |
|
7 import pvl.dns |
|
8 |
|
9 class HostZoneError(Exception): |
|
10 pass |
|
11 |
|
12 def resolve (origin, domain, name) : |
|
13 """ |
|
14 Resolve relative CNAME for label@origin -> alias@domain |
|
15 """ |
|
16 |
|
17 fqdn = pvl.dns.join(name, domain) |
|
18 |
|
19 if not origin: |
|
20 return fqdn |
|
21 |
|
22 elif domain == origin: |
|
23 return name |
|
24 |
|
25 elif fqdn.endswith('.' + origin): |
|
26 return pvl.dns.relative(origin, fqdn) |
|
27 |
|
28 elif domain: |
|
29 raise HostZoneError("{name}: domain {domain} out of zone {origin}".format(name=name, domain=domain, origin=origin)) |
|
30 |
|
31 else: |
|
32 raise HostZoneError("{name}: fqdn {fqdn} out of zone {origin}".format(name=name, fqdn=fqdn, origin=origin)) |
|
33 |
|
34 def host_forward (host, origin) : |
|
35 """ |
|
36 Yield ZoneRecords for hosts within the given zone origin |
|
37 """ |
|
38 |
|
39 try: |
|
40 label = resolve(origin, host.domain, host.name) |
|
41 except HostZoneError as error: |
|
42 log.info("%s: skip: %s", host, error) |
|
43 return |
|
44 |
|
45 if host.forward: |
|
46 forward = pvl.dns.zone.fqdn(host.forward) |
|
47 |
|
48 log.info("%s: forward: %s", host, forward) |
|
49 |
|
50 yield pvl.dns.ZoneRecord.CNAME(label, forward) |
|
51 return |
|
52 |
|
53 elif host.forward is not None: |
|
54 log.info("%s: skip forward", host) |
|
55 return |
|
56 |
|
57 # forward |
|
58 if host.ip : |
|
59 yield pvl.dns.ZoneRecord.A(label, host.ip) |
|
60 |
|
61 if host.ip6 : |
|
62 yield pvl.dns.ZoneRecord.AAAA(label, host.ip6) |
|
63 |
|
64 if host.location: |
|
65 location_alias, location_domain = host.location |
|
66 |
|
67 if not location_domain: |
|
68 location_domain = host.domain |
|
69 |
|
70 yield pvl.dns.ZoneRecord.CNAME(resolve(origin, location_domain, location_alias), label) |
|
71 |
|
72 for alias in host.alias4: |
|
73 yield pvl.dns.ZoneRecord.CNAME(resolve(origin, host.domain, alias), label) |
|
74 |
|
75 for alias in host.alias4: |
|
76 yield pvl.dns.ZoneRecord.A(resolve(origin, host.domain, alias), host.ip) |
|
77 |
|
78 for alias in host.alias6: |
|
79 yield pvl.dns.ZoneRecord.AAAA(resolve(origin, host.domain, alias), host.ip6) |
|
80 |
|
81 def host_reverse (host, prefix) : |
|
82 """ |
|
83 Yield (ip, fqnd) for host within given prefix. |
|
84 """ |
|
85 |
|
86 if prefix.version == 4 : |
|
87 ip = host.ip |
|
88 elif prefix.version == 6 : |
|
89 ip = host.ip6 |
|
90 else : |
|
91 raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version)) |
|
92 |
|
93 if not ip : |
|
94 log.debug("%s: no ip%d", host, prefix.version) |
|
95 return |
|
96 |
|
97 if ip not in prefix : |
|
98 log.debug("%s: %s out of prefix: %s", host, ip, prefix) |
|
99 return |
|
100 |
|
101 # relative label |
|
102 label = pvl.dns.reverse_label(prefix, ip) |
|
103 |
|
104 if host.reverse : |
|
105 alias = pvl.dns.zone.fqdn(host.reverse) |
|
106 |
|
107 log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias) |
|
108 |
|
109 yield ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias) |
|
110 |
|
111 elif host.reverse is None : |
|
112 fqdn = host.fqdn() |
|
113 |
|
114 log.info("%s %s[%s]: PTR %s", host, prefix, ip, fqdn) |
|
115 |
|
116 yield ip, pvl.dns.zone.ZoneRecord.PTR(label, fqdn) |
|
117 |
|
118 else : |
|
119 log.info("%s %s[%s]: omit", host, prefix, ip) |
|
120 |
|
121 def apply_hosts_forward (options, hosts, origin) : |
|
122 """ |
|
123 Generate DNS ZoneRecords for for hosts within the given zone origin. |
|
124 |
|
125 Yields ZoneRecords in Host-order |
|
126 """ |
|
127 |
|
128 if options.add_origin : |
|
129 yield pvl.dns.ZoneDirective.build(None, 'ORIGIN', origin) |
|
130 |
|
131 by_name = dict() |
|
132 by_cname = dict() |
|
133 |
|
134 for host in hosts: |
|
135 if not host.domain: |
|
136 log.debug("%s: skip without domain", host) |
|
137 |
|
138 for rr in host_forward(host, origin) : |
|
139 if rr.name in by_cname: |
|
140 raise HostZoneError(host, "{host}: CNAME {cname} conflict: {rr}".format(host=host, cname=by_cname[rr.name].name, rr=rr)) |
|
141 elif rr.type == 'CNAME' and rr.name in by_name: |
|
142 raise HostZoneError(host, "{host}: CNAME {cname} conflict: {rr}".format(host=host, cname=rr.name, rr=by_name[rr.name])) |
|
143 |
|
144 by_name[rr.name] = rr |
|
145 |
|
146 if rr.type == 'CNAME': |
|
147 by_cname[rr.name] = rr |
|
148 |
|
149 # preserve ordering |
|
150 yield rr |
|
151 |
|
152 def apply_hosts_reverse (options, hosts, prefix) : |
|
153 """ |
|
154 Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts. |
|
155 |
|
156 Yields ZoneRecords in IPAddress-order |
|
157 """ |
|
158 |
|
159 # collect data for records |
|
160 by_ip = dict() |
|
161 |
|
162 for host in hosts: |
|
163 for ip, rr in host_reverse(host, prefix) : |
|
164 if ip in by_ip : |
|
165 raise HostZoneError(host, "{host}: IP {ip} conflict: {other}".format(host=host, ip=ip, other=by_ip[ip])) |
|
166 |
|
167 # do not retain order |
|
168 by_ip[ip] = rr |
|
169 |
|
170 if options.unknown_host : |
|
171 # enumerate all of them |
|
172 iter_ips = prefix.iterhosts() |
|
173 else : |
|
174 iter_ips = sorted(by_ip) |
|
175 |
|
176 for ip in iter_ips : |
|
177 if ip in by_ip : |
|
178 yield by_ip[ip] |
|
179 |
|
180 elif options.unknown_host: |
|
181 # synthesize a record |
|
182 label = pvl.dns.reverse_label(prefix, ip) |
|
183 fqdn = pvl.dns.zone.fqdn(options.unknown_host, options.hosts_domain) |
|
184 |
|
185 log.info("%s %s[%s]: unused PTR %s", options.unknown_host, ip, prefix, fqdn) |
|
186 |
|
187 yield pvl.dns.zone.ZoneRecord.PTR(label, fqdn) |
|
188 |
|
189 else : |
|
190 continue |
|
191 |
|
192 import pvl.args |
|
193 import pvl.hosts.config |
|
194 |
|
195 import optparse |
|
196 |
|
197 def forward_main () : |
|
198 """ |
|
199 Generate bind zonefiles from host definitions. |
|
200 """ |
|
201 |
|
202 parser = optparse.OptionParser(forward_main.__doc__) |
|
203 parser.add_option_group(pvl.args.parser(parser)) |
|
204 parser.add_option_group(pvl.hosts.config.optparser(parser)) |
|
205 |
|
206 parser.add_option('--add-origin', action='store_true', |
|
207 help="Include $ORIGIN directive in zone") |
|
208 |
|
209 parser.add_option('--forward-zone', metavar='DOMAIN', |
|
210 help="Generate forward zone for domain") |
|
211 |
|
212 # input |
|
213 options, args = parser.parse_args() |
|
214 |
|
215 pvl.args.apply(options) |
|
216 |
|
217 hosts = pvl.hosts.apply(options, args) |
|
218 |
|
219 # process |
|
220 for rr in apply_hosts_forward(options, hosts, options.forward_zone): |
|
221 print unicode(rr) |
|
222 |
|
223 return 0 |
|
224 |
|
225 def reverse_main () : |
|
226 """ |
|
227 Generate bind zonefiles from host definitions. |
|
228 """ |
|
229 |
|
230 parser = optparse.OptionParser(reverse_main.__doc__) |
|
231 parser.add_option_group(pvl.args.parser(parser)) |
|
232 parser.add_option_group(pvl.hosts.config.optparser(parser)) |
|
233 |
|
234 parser.add_option('--reverse-zone', metavar='PREFIX', |
|
235 help="Generate reverse zone for prefix") |
|
236 |
|
237 parser.add_option('--unknown-host', metavar='NAME', |
|
238 help="Generate records for unused IPs") |
|
239 |
|
240 # input |
|
241 options, args = parser.parse_args() |
|
242 |
|
243 pvl.args.apply(options) |
|
244 |
|
245 hosts = pvl.hosts.apply(options, args) |
|
246 |
|
247 # process |
|
248 for rr in apply_hosts_reverse(options, hosts, pvl.dns.parse_prefix(options.reverse_zone)): |
|
249 print unicode(rr) |
|
250 |
|
251 return 0 |