source: trunk/server/lib/dnspython/examples/zonediff.py

Last change on this file was 745, checked in by Silvan Scherrer, 13 years ago

Samba Server: updated trunk to 3.6.0

File size: 10.7 KB
Line 
1#!/usr/bin/env python
2#
3# Small library and commandline tool to do logical diffs of zonefiles
4# ./zonediff -h gives you help output
5#
6# Requires dnspython to do all the heavy lifting
7#
8# (c)2009 Dennis Kaarsemaker <dennis@kaarsemaker.net>
9#
10# Permission to use, copy, modify, and distribute this software and its
11# documentation for any purpose with or without fee is hereby granted,
12# provided that the above copyright notice and this permission notice
13# appear in all copies.
14#
15# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
16# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
18# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
21# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22"""See diff_zones.__doc__ for more information"""
23
24__all__ = ['diff_zones', 'format_changes_plain', 'format_changes_html']
25
26try:
27 import dns.zone
28except ImportError:
29 import sys
30 sys.stderr.write("Please install dnspython")
31 sys.exit(1)
32
33def diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False):
34 """diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False) -> changes
35 Compares two dns.zone.Zone objects and returns a list of all changes
36 in the format (name, oldnode, newnode).
37
38 If ignore_ttl is true, a node will not be added to this list if the
39 only change is its TTL.
40
41 If ignore_soa is true, a node will not be added to this list if the
42 only changes is a change in a SOA Rdata set.
43
44 The returned nodes do include all Rdata sets, including unchanged ones.
45 """
46
47 changes = []
48 for name in zone1:
49 name = str(name)
50 n1 = zone1.get_node(name)
51 n2 = zone2.get_node(name)
52 if not n2:
53 changes.append((str(name), n1, n2))
54 elif _nodes_differ(n1, n2, ignore_ttl, ignore_soa):
55 changes.append((str(name), n1, n2))
56
57 for name in zone2:
58 n1 = zone1.get_node(name)
59 if not n1:
60 n2 = zone2.get_node(name)
61 changes.append((str(name), n1, n2))
62 return changes
63
64def _nodes_differ(n1, n2, ignore_ttl, ignore_soa):
65 if ignore_soa or not ignore_ttl:
66 # Compare datasets directly
67 for r in n1.rdatasets:
68 if ignore_soa and r.rdtype == dns.rdatatype.SOA:
69 continue
70 if r not in n2.rdatasets:
71 return True
72 if not ignore_ttl:
73 return r.ttl != n2.find_rdataset(r.rdclass, r.rdtype).ttl
74
75 for r in n2.rdatasets:
76 if ignore_soa and r.rdtype == dns.rdatatype.SOA:
77 continue
78 if r not in n1.rdatasets:
79 return True
80 else:
81 return n1 != n2
82
83def format_changes_plain(oldf, newf, changes, ignore_ttl=False):
84 """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str
85 Given 2 filenames and a list of changes from diff_zones, produce diff-like
86 output. If ignore_ttl is True, TTL-only changes are not displayed"""
87
88 ret = "--- %s\n+++ %s\n" % (oldf, newf)
89 for name, old, new in changes:
90 ret += "@ %s\n" % name
91 if not old:
92 for r in new.rdatasets:
93 ret += "+ %s\n" % str(r).replace('\n','\n+ ')
94 elif not new:
95 for r in old.rdatasets:
96 ret += "- %s\n" % str(r).replace('\n','\n+ ')
97 else:
98 for r in old.rdatasets:
99 if r not in new.rdatasets or (r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
100 ret += "- %s\n" % str(r).replace('\n','\n+ ')
101 for r in new.rdatasets:
102 if r not in old.rdatasets or (r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
103 ret += "+ %s\n" % str(r).replace('\n','\n+ ')
104 return ret
105
106def format_changes_html(oldf, newf, changes, ignore_ttl=False):
107 """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str
108 Given 2 filenames and a list of changes from diff_zones, produce nice html
109 output. If ignore_ttl is True, TTL-only changes are not displayed"""
110
111 ret = '''<table class="zonediff">
112 <thead>
113 <tr>
114 <th>&nbsp;</th>
115 <th class="old">%s</th>
116 <th class="new">%s</th>
117 </tr>
118 </thead>
119 <tbody>\n''' % (oldf, newf)
120
121 for name, old, new in changes:
122 ret += ' <tr class="rdata">\n <td class="rdname">%s</td>\n' % name
123 if not old:
124 for r in new.rdatasets:
125 ret += ' <td class="old">&nbsp;</td>\n <td class="new">%s</td>\n' % str(r).replace('\n','<br />')
126 elif not new:
127 for r in old.rdatasets:
128 ret += ' <td class="old">%s</td>\n <td class="new">&nbsp;</td>\n' % str(r).replace('\n','<br />')
129 else:
130 ret += ' <td class="old">'
131 for r in old.rdatasets:
132 if r not in new.rdatasets or (r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
133 ret += str(r).replace('\n','<br />')
134 ret += '</td>\n'
135 ret += ' <td class="new">'
136 for r in new.rdatasets:
137 if r not in old.rdatasets or (r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
138 ret += str(r).replace('\n','<br />')
139 ret += '</td>\n'
140 ret += ' </tr>\n'
141 return ret + ' </tbody>\n</table>'
142
143# Make this module usable as a script too.
144if __name__ == '__main__':
145 import optparse
146 import subprocess
147 import sys
148 import traceback
149
150 usage = """%prog zonefile1 zonefile2 - Show differences between zones in a diff-like format
151%prog [--git|--bzr|--rcs] zonefile rev1 [rev2] - Show differences between two revisions of a zonefile
152
153The differences shown will be logical differences, not textual differences.
154"""
155 p = optparse.OptionParser(usage=usage)
156 p.add_option('-s', '--ignore-soa', action="store_true", default=False, dest="ignore_soa",
157 help="Ignore SOA-only changes to records")
158 p.add_option('-t', '--ignore-ttl', action="store_true", default=False, dest="ignore_ttl",
159 help="Ignore TTL-only changes to Rdata")
160 p.add_option('-T', '--traceback', action="store_true", default=False, dest="tracebacks",
161 help="Show python tracebacks when errors occur")
162 p.add_option('-H', '--html', action="store_true", default=False, dest="html",
163 help="Print HTML output")
164 p.add_option('-g', '--git', action="store_true", default=False, dest="use_git",
165 help="Use git revisions instead of real files")
166 p.add_option('-b', '--bzr', action="store_true", default=False, dest="use_bzr",
167 help="Use bzr revisions instead of real files")
168 p.add_option('-r', '--rcs', action="store_true", default=False, dest="use_rcs",
169 help="Use rcs revisions instead of real files")
170 opts, args = p.parse_args()
171 opts.use_vc = opts.use_git or opts.use_bzr or opts.use_rcs
172
173 def _open(what, err):
174 if isinstance(what, basestring):
175 # Open as normal file
176 try:
177 return open(what, 'rb')
178 except:
179 sys.stderr.write(err + "\n")
180 if opts.tracebacks:
181 traceback.print_exc()
182 else:
183 # Must be a list, open subprocess
184 try:
185 proc = subprocess.Popen(what, stdout=subprocess.PIPE)
186 proc.wait()
187 if proc.returncode == 0:
188 return proc.stdout
189 sys.stderr.write(err + "\n")
190 except:
191 sys.stderr.write(err + "\n")
192 if opts.tracebacks:
193 traceback.print_exc()
194
195 if not opts.use_vc and len(args) != 2:
196 p.print_help()
197 sys.exit(64)
198 if opts.use_vc and len(args) not in (2,3):
199 p.print_help()
200 sys.exit(64)
201
202 # Open file desriptors
203 if not opts.use_vc:
204 oldn, newn = args
205 else:
206 if len(args) == 3:
207 filename, oldr, newr = args
208 oldn = "%s:%s" % (oldr, filename)
209 newn = "%s:%s" % (newr, filename)
210 else:
211 filename, oldr = args
212 newr = None
213 oldn = "%s:%s" % (oldr, filename)
214 newn = filename
215
216
217 old, new = None, None
218 oldz, newz = None, None
219 if opts.use_bzr:
220 old = _open(["bzr", "cat", "-r" + oldr, filename],
221 "Unable to retrieve revision %s of %s" % (oldr, filename))
222 if newr != None:
223 new = _open(["bzr", "cat", "-r" + newr, filename],
224 "Unable to retrieve revision %s of %s" % (newr, filename))
225 elif opts.use_git:
226 old = _open(["git", "show", oldn],
227 "Unable to retrieve revision %s of %s" % (oldr, filename))
228 if newr != None:
229 new = _open(["git", "show", newn],
230 "Unable to retrieve revision %s of %s" % (newr, filename))
231 elif opts.use_rcs:
232 old = _open(["co", "-q", "-p", "-r" + oldr, filename],
233 "Unable to retrieve revision %s of %s" % (oldr, filename))
234 if newr != None:
235 new = _open(["co", "-q", "-p", "-r" + newr, filename],
236 "Unable to retrieve revision %s of %s" % (newr, filename))
237 if not opts.use_vc:
238 old = _open(oldn, "Unable to open %s" % oldn)
239 if not opts.use_vc or newr == None:
240 new = _open(newn, "Unable to open %s" % newn)
241
242 if not old or not new:
243 sys.exit(65)
244
245 # Parse the zones
246 try:
247 oldz = dns.zone.from_file(old, origin = '.', check_origin=False)
248 except dns.exception.DNSException:
249 sys.stderr.write("Incorrect zonefile: %s\n", old)
250 if opts.tracebacks:
251 traceback.print_exc()
252 try:
253 newz = dns.zone.from_file(new, origin = '.', check_origin=False)
254 except dns.exception.DNSException:
255 sys.stderr.write("Incorrect zonefile: %s\n" % new)
256 if opts.tracebacks:
257 traceback.print_exc()
258 if not oldz or not newz:
259 sys.exit(65)
260
261 changes = diff_zones(oldz, newz, opts.ignore_ttl, opts.ignore_soa)
262 changes.sort()
263
264 if not changes:
265 sys.exit(0)
266 if opts.html:
267 print format_changes_html(oldn, newn, changes, opts.ignore_ttl)
268 else:
269 print format_changes_plain(oldn, newn, changes, opts.ignore_ttl)
270 sys.exit(1)
Note: See TracBrowser for help on using the repository browser.