Move dnspython to third_party.
[bbaumbach/samba-autobuild/.git] / third_party / dnspython / examples / zonediff.py
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
26 try:
27     import dns.zone
28 except ImportError:
29     import sys
30     sys.stderr.write("Please install dnspython")
31     sys.exit(1)
32
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).
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
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:
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
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"""
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
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"""
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.
144 if __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
153 The 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)