3
# Small library and commandline tool to do logical diffs of zonefiles
4
# ./zonediff -h gives you help output
6
# Requires dnspython to do all the heavy lifting
8
# (c)2009 Dennis Kaarsemaker <dennis@kaarsemaker.net>
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.
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"""
24
__all__ = ['diff_zones', 'format_changes_plain', 'format_changes_html']
30
sys.stderr.write("Please install dnspython")
33
def 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).
38
If ignore_ttl is true, a node will not be added to this list if the
39
only change is its TTL.
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.
44
The returned nodes do include all Rdata sets, including unchanged ones.
50
n1 = zone1.get_node(name)
51
n2 = zone2.get_node(name)
53
changes.append((str(name), n1, n2))
54
elif _nodes_differ(n1, n2, ignore_ttl, ignore_soa):
55
changes.append((str(name), n1, n2))
58
n1 = zone1.get_node(name)
60
n2 = zone2.get_node(name)
61
changes.append((str(name), n1, n2))
64
def _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:
70
if r not in n2.rdatasets:
73
return r.ttl != n2.find_rdataset(r.rdclass, r.rdtype).ttl
75
for r in n2.rdatasets:
76
if ignore_soa and r.rdtype == dns.rdatatype.SOA:
78
if r not in n1.rdatasets:
83
def 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"""
88
ret = "--- %s\n+++ %s\n" % (oldf, newf)
89
for name, old, new in changes:
90
ret += "@ %s\n" % name
92
for r in new.rdatasets:
93
ret += "+ %s\n" % str(r).replace('\n','\n+ ')
95
for r in old.rdatasets:
96
ret += "- %s\n" % str(r).replace('\n','\n+ ')
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+ ')
106
def 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"""
111
ret = '''<table class="zonediff">
115
<th class="old">%s</th>
116
<th class="new">%s</th>
119
<tbody>\n''' % (oldf, newf)
121
for name, old, new in changes:
122
ret += ' <tr class="rdata">\n <td class="rdname">%s</td>\n' % name
124
for r in new.rdatasets:
125
ret += ' <td class="old"> </td>\n <td class="new">%s</td>\n' % str(r).replace('\n','<br />')
127
for r in old.rdatasets:
128
ret += ' <td class="old">%s</td>\n <td class="new"> </td>\n' % str(r).replace('\n','<br />')
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 />')
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 />')
141
return ret + ' </tbody>\n</table>'
143
# Make this module usable as a script too.
144
if __name__ == '__main__':
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
153
The differences shown will be logical differences, not textual differences.
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
173
def _open(what, err):
174
if isinstance(what, basestring):
175
# Open as normal file
177
return open(what, 'rb')
179
sys.stderr.write(err + "\n")
181
traceback.print_exc()
183
# Must be a list, open subprocess
185
proc = subprocess.Popen(what, stdout=subprocess.PIPE)
187
if proc.returncode == 0:
189
sys.stderr.write(err + "\n")
191
sys.stderr.write(err + "\n")
193
traceback.print_exc()
195
if not opts.use_vc and len(args) != 2:
198
if opts.use_vc and len(args) not in (2,3):
202
# Open file desriptors
207
filename, oldr, newr = args
208
oldn = "%s:%s" % (oldr, filename)
209
newn = "%s:%s" % (newr, filename)
211
filename, oldr = args
213
oldn = "%s:%s" % (oldr, filename)
217
old, new = None, None
218
oldz, newz = None, None
220
old = _open(["bzr", "cat", "-r" + oldr, filename],
221
"Unable to retrieve revision %s of %s" % (oldr, filename))
223
new = _open(["bzr", "cat", "-r" + newr, filename],
224
"Unable to retrieve revision %s of %s" % (newr, filename))
226
old = _open(["git", "show", oldn],
227
"Unable to retrieve revision %s of %s" % (oldr, filename))
229
new = _open(["git", "show", newn],
230
"Unable to retrieve revision %s of %s" % (newr, filename))
232
old = _open(["co", "-q", "-p", "-r" + oldr, filename],
233
"Unable to retrieve revision %s of %s" % (oldr, filename))
235
new = _open(["co", "-q", "-p", "-r" + newr, filename],
236
"Unable to retrieve revision %s of %s" % (newr, filename))
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)
242
if not old or not new:
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)
251
traceback.print_exc()
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)
257
traceback.print_exc()
258
if not oldz or not newz:
261
changes = diff_zones(oldz, newz, opts.ignore_ttl, opts.ignore_soa)
267
print format_changes_html(oldn, newn, changes, opts.ignore_ttl)
269
print format_changes_plain(oldn, newn, changes, opts.ignore_ttl)