s3:net_idmap_delete do not lock two records at the same time
[Samba/gebeck_regimport.git] / lib / dnspython / examples / zonediff.py
blobad81fb1d2d4868534dad2230d3776b9449722864
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
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']
26 try:
27 import dns.zone
28 except ImportError:
29 import sys
30 sys.stderr.write("Please install dnspython")
31 sys.exit(1)
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.
45 """
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))
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
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
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
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
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
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">
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)
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>'
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
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
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()
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)
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
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)
242 if not old or not new:
243 sys.exit(65)
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)
261 changes = diff_zones(oldz, newz, opts.ignore_ttl, opts.ignore_soa)
262 changes.sort()
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)