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
)