95 # apply |
95 # apply |
96 pvl.args.apply(options, prog) |
96 pvl.args.apply(options, prog) |
97 |
97 |
98 return options, args |
98 return options, args |
99 |
99 |
|
100 def apply_zone_input (options, args) : |
|
101 """ |
|
102 Yield ZoneLine, ZoneRecord pairs from files. |
|
103 """ |
|
104 |
|
105 for file in pvl.args.apply_files(args, 'r', options.input_charset) : |
|
106 log.info("Reading zone: %s", file) |
|
107 |
|
108 for line, record in pvl.dns.zone.ZoneLine.load(file, |
|
109 line_timestamp_prefix = options.input_line_date, |
|
110 ) : |
|
111 yield line, record |
|
112 |
|
113 # TODO: --check-types to limit this to A/AAAA/CNAME etc |
100 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) : |
114 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) : |
101 """ |
115 """ |
102 Parse host/IP pairs from the zone, and verify that they are unique. |
116 Parse host/IP pairs from the zone, and verify that they are unique. |
103 |
117 |
104 As an exception, names listed in the given whitelist may have multiple IPs. |
118 As an exception, names listed in the given whitelist may have multiple IPs. |
109 |
123 |
110 fail = None |
124 fail = None |
111 |
125 |
112 last_name = None |
126 last_name = None |
113 |
127 |
114 for r in zone : |
128 for l, r in zone : |
115 name = r.name or last_name |
129 if r : |
116 |
130 name = r.name or last_name |
117 name = (r.origin, name) |
131 |
118 |
132 name = (r.origin, name) |
119 # name |
133 |
120 if r.type not in whitelist_types : |
134 # name |
121 if name not in by_name : |
135 if r.type not in whitelist_types : |
122 by_name[name] = r |
136 if name not in by_name : |
123 |
137 by_name[name] = r |
124 elif r.name in whitelist : |
138 |
125 log.debug("Duplicate whitelist entry: %s", r) |
139 elif r.name in whitelist : |
126 |
140 log.debug("Duplicate whitelist entry: %s", r) |
127 else : |
141 |
128 # fail! |
142 else : |
129 log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name]) |
143 # fail! |
130 fail = True |
144 log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name]) |
131 |
145 fail = True |
132 # ip |
146 |
133 if r.type == 'A' : |
147 # ip |
134 ip, = r.data |
148 if r.type == 'A' : |
135 |
149 ip, = r.data |
136 if ip not in by_ip : |
150 |
137 by_ip[ip] = r |
151 if ip not in by_ip : |
138 |
152 by_ip[ip] = r |
139 else : |
153 |
140 # fail! |
154 else : |
141 log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip]) |
155 # fail! |
142 fail = True |
156 log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip]) |
143 |
157 fail = True |
144 return fail |
158 |
145 |
159 if fail : |
146 def process_zone_soa (soa, serial) : |
160 log.error("Check failed, see warnings") |
147 return pvl.dns.zone.SOA( |
161 sys.exit(2) |
148 soa.master, soa.contact, |
162 |
149 serial, soa.refresh, soa.retry, soa.expire, soa.nxttl |
163 yield l, r |
150 ) |
|
151 |
164 |
152 def process_zone_serial (zone, serial) : |
165 def process_zone_serial (zone, serial) : |
153 for rr in zone : |
166 """ |
154 if rr.type == 'SOA' : |
167 Update the serial in the SOA record. |
|
168 """ |
|
169 |
|
170 for line, rr in zone : |
|
171 if rr and rr.type == 'SOA' : |
155 # XXX: as SOA record.. |
172 # XXX: as SOA record.. |
156 yield process_zone_soa(pvl.dns.zone.SOA.parse(rr.line), serial) |
173 soa = pvl.dns.zone.SOA.parse(line) |
157 else : |
174 |
158 yield rr |
175 yield line, pvl.dns.zone.SOA( |
|
176 soa.master, soa.contact, |
|
177 serial, soa.refresh, soa.retry, soa.expire, soa.nxttl |
|
178 ) |
|
179 else : |
|
180 yield line, rr |
159 |
181 |
160 def process_zone_forwards (zone, txt=False, mx=False) : |
182 def process_zone_forwards (zone, txt=False, mx=False) : |
161 """ |
183 """ |
162 Process zone data -> forward zone data. |
184 Process zone data -> forward zone data. |
163 """ |
185 """ |
164 |
186 |
165 for r in zone : |
187 for line, r in zone : |
166 yield r |
188 yield line, r |
167 |
189 |
168 if r.type == 'A' : |
190 if r and r.type == 'A' : |
169 if txt : |
191 if txt : |
170 # comment? |
192 # comment? |
171 comment = r.line.comment |
193 comment = r.line.comment |
172 |
194 |
173 if comment : |
195 if comment : |
174 yield ZoneRecord.TXT(None, comment, ttl=r.ttl) |
196 yield line, ZoneRecord.TXT(None, comment, ttl=r.ttl) |
175 |
197 |
176 |
198 |
177 # XXX: RP, do we need it? |
199 # XXX: RP, do we need it? |
178 |
200 |
179 if mx : |
201 if mx : |
180 # XXX: is this even a good idea? |
202 # XXX: is this even a good idea? |
181 yield ZoneRecord.MX(None, 10, mx, ttl=r.ttl) |
203 yield line, ZoneRecord.MX(None, 10, mx, ttl=r.ttl) |
182 |
204 |
183 def process_zone_meta (zone, ignore=None) : |
205 def process_zone_meta (zone, ignore=None) : |
184 """ |
206 """ |
185 Process zone metadata -> output. |
207 Process zone metadata -> output. |
186 """ |
208 """ |
187 |
209 |
188 TIMESTAMP_FORMAT = '%Y/%m/%d' |
210 TIMESTAMP_FORMAT = '%Y/%m/%d' |
189 |
211 |
190 for r in zone : |
212 for line, r in zone : |
191 if ignore and r.name in ignore : |
213 if ignore and r.name in ignore : |
192 # skip |
214 # skip |
193 log.debug("Ignore record: %s", r) |
215 log.debug("Ignore record: %s", r) |
194 continue |
216 continue |
195 |
217 |
197 if r.type == 'A' : |
219 if r.type == 'A' : |
198 # timestamp? |
220 # timestamp? |
199 timestamp = r.line.timestamp |
221 timestamp = r.line.timestamp |
200 |
222 |
201 if timestamp : |
223 if timestamp : |
202 yield ZoneRecord.TXT(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl) |
224 yield line, ZoneRecord.TXT(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl) |
203 |
225 |
204 def process_zone_reverse (zone, origin, domain) : |
226 def process_zone_reverse (zone, origin, domain) : |
205 """ |
227 """ |
206 Process zone data -> reverse zone data. |
228 Process zone data -> reverse zone data. |
207 """ |
229 """ |
208 |
230 |
209 name = None |
231 for line, r in zone : |
210 |
232 if r and r.type == 'A' : |
211 for r in zone : |
|
212 # keep name from previous.. |
|
213 if r.name : |
|
214 name = r.name |
|
215 |
|
216 if r.type == 'A' : |
|
217 ip, = r.data |
233 ip, = r.data |
218 ptr = reverse_ipv4(ip) |
234 ptr = reverse_ipv4(ip) |
219 |
235 |
220 elif r.type == 'AAAA' : |
236 elif r and r.type == 'AAAA' : |
221 ip, = r.data |
237 ip, = r.data |
222 ptr = reverse_ipv6(ip) |
238 ptr = reverse_ipv6(ip) |
223 |
239 |
224 else : |
240 else : |
|
241 yield line, r |
225 continue |
242 continue |
226 |
243 |
227 # verify |
244 # verify |
228 if zone and ptr.endswith(origin) : |
245 if zone and ptr.endswith(origin) : |
229 ptr = ptr[:-(len(origin) + 1)] |
246 ptr = ptr[:-(len(origin) + 1)] |
234 |
251 |
235 # domain to use |
252 # domain to use |
236 host_domain = r.origin or domain |
253 host_domain = r.origin or domain |
237 host_fqdn = fqdn(name, host_domain) |
254 host_fqdn = fqdn(name, host_domain) |
238 |
255 |
239 yield ZoneRecord.PTR(ptr, host_fqdn) |
256 yield line, ZoneRecord.PTR(ptr, host_fqdn) |
240 |
257 |
241 def write_zone_records (file, zone) : |
258 def apply_zone_output (options, zone) : |
242 for r in zone : |
259 """ |
243 file.write(unicode(r)) |
260 Write out the resulting zonefile. |
|
261 """ |
|
262 |
|
263 file = pvl.args.apply_file(options.output, 'w', options.output_charset) |
|
264 |
|
265 for line, r in zone : |
|
266 if r : |
|
267 file.write(unicode(r)) |
|
268 else : |
|
269 file.write(line.line) |
244 file.write('\n') |
270 file.write('\n') |
245 |
271 |
246 def main (argv) : |
272 def main (argv) : |
247 options, args = parse_options(argv) |
273 options, args = parse_options(argv) |
248 |
274 |
249 # open files, default to stdout |
275 # input |
250 input_files = pvl.args.apply_files(args, 'r', options.input_charset) |
276 zone = apply_zone_input(options, args) |
251 |
277 |
252 # process zone data |
|
253 zone = [] |
|
254 |
|
255 for file in input_files : |
|
256 log.info("Reading zone: %s", file) |
|
257 |
|
258 zone += list(pvl.dns.zone.ZoneRecord.load(file, |
|
259 line_timestamp_prefix = options.input_line_date, |
|
260 )) |
|
261 |
|
262 # check? |
|
263 if options.check_hosts : |
278 if options.check_hosts : |
264 whitelist = set(options.check_exempt) |
279 whitelist = set(options.check_exempt) |
265 |
280 |
266 log.debug("checking hosts; whitelist=%r", whitelist) |
281 log.info("Checking hosts: whitelist=%r", whitelist) |
267 |
282 |
268 if check_zone_hosts(zone, whitelist=whitelist) : |
283 zone = list(check_zone_hosts(zone, whitelist=whitelist)) |
269 log.warn("Hosts check failed") |
|
270 return 2 |
|
271 |
|
272 else : |
|
273 log.info("Hosts check OK") |
|
274 |
284 |
275 if options.serial : |
285 if options.serial : |
276 log.info("Set zone serial: %s", options.serial) |
286 log.info("Set zone serial: %s", options.serial) |
277 |
287 |
278 zone = list(process_zone_serial(zone, serial=options.serial)) |
288 zone = list(process_zone_serial(zone, serial=options.serial)) |
279 |
289 |
280 # output file |
|
281 output = open_file(options.output, 'w', options.output_charset) |
|
282 |
|
283 if options.forward_zone : |
290 if options.forward_zone : |
284 log.info("Write forward zone: %s", output) |
291 log.info("Generate forward zone...") |
285 |
292 |
286 zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx)) |
293 zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx)) |
287 |
294 |
288 elif options.meta_zone : |
295 if options.meta_zone : |
289 log.info("Write metadata zone: %s", output) |
296 log.info("Generate metadata zone...") |
290 |
297 |
291 if not options.input_line_date : |
298 if not options.input_line_date : |
292 log.error("--meta-zone requires --input-line-date") |
299 log.error("--meta-zone requires --input-line-date") |
293 return 1 |
300 return 1 |
294 |
301 |
295 zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore))) |
302 zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore))) |
296 |
303 |
297 elif options.reverse_zone : |
304 if options.reverse_zone : |
298 if ':' in options.reverse_zone : |
305 if ':' in options.reverse_zone : |
299 # IPv6 |
306 # IPv6 |
300 origin = reverse_ipv6(options.reverse_zone) |
307 origin = reverse_ipv6(options.reverse_zone) |
301 |
308 |
302 else : |
309 else : |