| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- #!/usr/bin/env python
- #
- # Small library and commandline tool to do logical diffs of zonefiles
- # ./zonediff -h gives you help output
- #
- # Requires dnspython to do all the heavy lifting
- #
- # (c)2009 Dennis Kaarsemaker <dennis@kaarsemaker.net>
- #
- # Permission to use, copy, modify, and distribute this software and its
- # documentation for any purpose with or without fee is hereby granted,
- # provided that the above copyright notice and this permission notice
- # appear in all copies.
- #
- # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
- # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- """See diff_zones.__doc__ for more information"""
- from __future__ import print_function
- __all__ = ['diff_zones', 'format_changes_plain', 'format_changes_html']
- try:
- import dns.zone
- except ImportError:
- raise SystemExit("Please install dnspython")
- def diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False):
- """diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False) -> changes
- Compares two dns.zone.Zone objects and returns a list of all changes
- in the format (name, oldnode, newnode).
- If ignore_ttl is true, a node will not be added to this list if the
- only change is its TTL.
- If ignore_soa is true, a node will not be added to this list if the
- only changes is a change in a SOA Rdata set.
- The returned nodes do include all Rdata sets, including unchanged ones.
- """
- changes = []
- for name in zone1:
- name = str(name)
- n1 = zone1.get_node(name)
- n2 = zone2.get_node(name)
- if not n2:
- changes.append((str(name), n1, n2))
- elif _nodes_differ(n1, n2, ignore_ttl, ignore_soa):
- changes.append((str(name), n1, n2))
- for name in zone2:
- n1 = zone1.get_node(name)
- if not n1:
- n2 = zone2.get_node(name)
- changes.append((str(name), n1, n2))
- return changes
- def _nodes_differ(n1, n2, ignore_ttl, ignore_soa):
- if ignore_soa or not ignore_ttl:
- # Compare datasets directly
- for r in n1.rdatasets:
- if ignore_soa and r.rdtype == dns.rdatatype.SOA:
- continue
- if r not in n2.rdatasets:
- return True
- if not ignore_ttl:
- return r.ttl != n2.find_rdataset(r.rdclass, r.rdtype).ttl
- for r in n2.rdatasets:
- if ignore_soa and r.rdtype == dns.rdatatype.SOA:
- continue
- if r not in n1.rdatasets:
- return True
- else:
- return n1 != n2
- def format_changes_plain(oldf, newf, changes, ignore_ttl=False):
- """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str
- Given 2 filenames and a list of changes from diff_zones, produce diff-like
- output. If ignore_ttl is True, TTL-only changes are not displayed"""
- ret = "--- %s\n+++ %s\n" % (oldf, newf)
- for name, old, new in changes:
- ret += "@ %s\n" % name
- if not old:
- for r in new.rdatasets:
- ret += "+ %s\n" % str(r).replace('\n', '\n+ ')
- elif not new:
- for r in old.rdatasets:
- ret += "- %s\n" % str(r).replace('\n', '\n+ ')
- else:
- for r in old.rdatasets:
- if r not in new.rdatasets or (
- r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and
- not ignore_ttl
- ):
- ret += "- %s\n" % str(r).replace('\n', '\n+ ')
- for r in new.rdatasets:
- if r not in old.rdatasets or (
- r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and
- not ignore_ttl
- ):
- ret += "+ %s\n" % str(r).replace('\n', '\n+ ')
- return ret
- def format_changes_html(oldf, newf, changes, ignore_ttl=False):
- """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str
- Given 2 filenames and a list of changes from diff_zones, produce nice html
- output. If ignore_ttl is True, TTL-only changes are not displayed"""
- ret = '''<table class="zonediff">
- <thead>
- <tr>
- <th> </th>
- <th class="old">%s</th>
- <th class="new">%s</th>
- </tr>
- </thead>
- <tbody>\n''' % (oldf, newf)
- for name, old, new in changes:
- ret += ' <tr class="rdata">\n <td class="rdname">%s</td>\n' % name
- if not old:
- for r in new.rdatasets:
- ret += (
- ' <td class="old"> </td>\n'
- ' <td class="new">%s</td>\n'
- ) % str(r).replace('\n', '<br />')
- elif not new:
- for r in old.rdatasets:
- ret += (
- ' <td class="old">%s</td>\n'
- ' <td class="new"> </td>\n'
- ) % str(r).replace('\n', '<br />')
- else:
- ret += ' <td class="old">'
- for r in old.rdatasets:
- if r not in new.rdatasets or (
- r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and
- not ignore_ttl
- ):
- ret += str(r).replace('\n', '<br />')
- ret += '</td>\n'
- ret += ' <td class="new">'
- for r in new.rdatasets:
- if r not in old.rdatasets or (
- r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and
- not ignore_ttl
- ):
- ret += str(r).replace('\n', '<br />')
- ret += '</td>\n'
- ret += ' </tr>\n'
- return ret + ' </tbody>\n</table>'
- # Make this module usable as a script too.
- def main():
- import argparse
- import subprocess
- import sys
- import traceback
- usage = """%prog zonefile1 zonefile2 - Show differences between zones in a diff-like format
- %prog [--git|--bzr|--rcs] zonefile rev1 [rev2] - Show differences between two revisions of a zonefile
- The differences shown will be logical differences, not textual differences.
- """
- p = argparse.ArgumentParser(usage=usage)
- p.add_argument('-s', '--ignore-soa', action="store_true", default=False, dest="ignore_soa",
- help="Ignore SOA-only changes to records")
- p.add_argument('-t', '--ignore-ttl', action="store_true", default=False, dest="ignore_ttl",
- help="Ignore TTL-only changes to Rdata")
- p.add_argument('-T', '--traceback', action="store_true", default=False, dest="tracebacks",
- help="Show python tracebacks when errors occur")
- p.add_argument('-H', '--html', action="store_true", default=False, dest="html",
- help="Print HTML output")
- p.add_argument('-g', '--git', action="store_true", default=False, dest="use_git",
- help="Use git revisions instead of real files")
- p.add_argument('-b', '--bzr', action="store_true", default=False, dest="use_bzr",
- help="Use bzr revisions instead of real files")
- p.add_argument('-r', '--rcs', action="store_true", default=False, dest="use_rcs",
- help="Use rcs revisions instead of real files")
- opts, args = p.parse_args()
- opts.use_vc = opts.use_git or opts.use_bzr or opts.use_rcs
- def _open(what, err):
- if isinstance(what, list):
- # Must be a list, open subprocess
- try:
- proc = subprocess.Popen(what, stdout=subprocess.PIPE)
- proc.wait()
- if proc.returncode == 0:
- return proc.stdout
- sys.stderr.write(err + "\n")
- except Exception:
- sys.stderr.write(err + "\n")
- if opts.tracebacks:
- traceback.print_exc()
- else:
- # Open as normal file
- try:
- return open(what, 'rb')
- except IOError:
- sys.stderr.write(err + "\n")
- if opts.tracebacks:
- traceback.print_exc()
- if not opts.use_vc and len(args) != 2:
- p.print_help()
- sys.exit(64)
- if opts.use_vc and len(args) not in (2, 3):
- p.print_help()
- sys.exit(64)
- # Open file descriptors
- if not opts.use_vc:
- oldn, newn = args
- else:
- if len(args) == 3:
- filename, oldr, newr = args
- oldn = "%s:%s" % (oldr, filename)
- newn = "%s:%s" % (newr, filename)
- else:
- filename, oldr = args
- newr = None
- oldn = "%s:%s" % (oldr, filename)
- newn = filename
- old, new = None, None
- oldz, newz = None, None
- if opts.use_bzr:
- old = _open(["bzr", "cat", "-r" + oldr, filename],
- "Unable to retrieve revision %s of %s" % (oldr, filename))
- if newr != None:
- new = _open(["bzr", "cat", "-r" + newr, filename],
- "Unable to retrieve revision %s of %s" % (newr, filename))
- elif opts.use_git:
- old = _open(["git", "show", oldn],
- "Unable to retrieve revision %s of %s" % (oldr, filename))
- if newr != None:
- new = _open(["git", "show", newn],
- "Unable to retrieve revision %s of %s" % (newr, filename))
- elif opts.use_rcs:
- old = _open(["co", "-q", "-p", "-r" + oldr, filename],
- "Unable to retrieve revision %s of %s" % (oldr, filename))
- if newr != None:
- new = _open(["co", "-q", "-p", "-r" + newr, filename],
- "Unable to retrieve revision %s of %s" % (newr, filename))
- if not opts.use_vc:
- old = _open(oldn, "Unable to open %s" % oldn)
- if not opts.use_vc or newr is None:
- new = _open(newn, "Unable to open %s" % newn)
- if not old or not new:
- sys.exit(65)
- # Parse the zones
- try:
- oldz = dns.zone.from_file(old, origin='.', check_origin=False)
- except dns.exception.DNSException:
- sys.stderr.write("Incorrect zonefile: %s\n", old)
- if opts.tracebacks:
- traceback.print_exc()
- try:
- newz = dns.zone.from_file(new, origin='.', check_origin=False)
- except dns.exception.DNSException:
- sys.stderr.write("Incorrect zonefile: %s\n" % new)
- if opts.tracebacks:
- traceback.print_exc()
- if not oldz or not newz:
- sys.exit(65)
- changes = diff_zones(oldz, newz, opts.ignore_ttl, opts.ignore_soa)
- changes.sort()
- if not changes:
- sys.exit(0)
- if opts.html:
- print(format_changes_html(oldn, newn, changes, opts.ignore_ttl))
- else:
- print(format_changes_plain(oldn, newn, changes, opts.ignore_ttl))
- sys.exit(1)
- if __name__ == '__main__':
- main()
|