dns-sprockets 2.0.0
Command-line DNS Zone validation tool
License: Apache License, Version 2.0
Keywords: DNS zone validation

**dns_sprockets**
=================

Overview
--------

**dns_sprockets** is a command-line framework for loading and validating DNS zones.
It is written in Python and uses the excellent `dnspython <http://www.dnspython.org>`_
library for much of its functionality.

"Why call it dns_sprockets?"  Well, if you think of a DNS zone as being a chain 
of resource records (thinking especially of NSEC3 records), this application is a
set of sprockets that tests every link of the chain.  *Hey, it kind of works ;)*

Audience
''''''''

Possible users include DNS code developers and quality assurance, internet 
customer service, system administrators, and end-users who are interested to 
know if their DNS zones are valid.

Features
''''''''

The command-line tool returns useful return codes, supporting automated build
systems.  It is built around the concept of plug-ins for implementing both the
zone loading and zone validating functions, and easily allows end-users the
ability to define new loaders and validators.

* Zones may be loaded using various means.  The framework supports "loader" 
  plug-ins that pull DNS zone data from any source.  Initially provided are
  'File' and 'Xfr' plugins to pull zone data from host files and XFR servers,
  respectively.

* Validations are modular pieces of code that may be selectively enabled.  The
  framework supports "validator" plug-ins that operate in one of four views:

  - Zone: Some aspect of the entire zone is validated.
  - Node: Every node (i.e. list of RRSets with the same owner name) can be validated.
  - RRSet: Every RRSet can be validated.
  - Record: Every DNS record can be validated.

  Initially provided in this package are some basic zone validators, and a
  fairly complete set of DNSSEC-type zone validators.

The Node, RRSet, and Record views may be optionally filtered by one or more 
resource record types, to simplify and focus the validation code.  Additionally,
each validator can be marked to run for non-DNSSEC zones, NSEC1-style DNSSEC
zones, or NSEC3-style DNSSEC zones. 

Installation
------------

Easy!  Use pip (preferably in a virtual python environment)::

    $ pip install dns_sprockets

Usage
-----

Once installed, simply issue the dns_sprockets command.  For example to get a
usage message::

    $ dns_sprockets --help

    # dns_sprockets (1.1.8) - A DNS Zone validation tool
    usage: dns_sprockets.py [-h] [-z s] [-l s] [-s s] [-i s] [-x s] [-d s] [-f s]
                            [-e] [-v]

    optional arguments:
      -h, --help            show this help message and exit
      -z s, --zone s        Name of zone to validate [www.ultradns.com]
      -l s, --loader s      Zone loader method to use (one of: file, xfr) [xfr]
      -s s, --source s      Loader source to use [127.0.0.1#53]
      -i s, --include s     Only include this validator (can use multiple times)
      -x s, --exclude s     Exclude this validator (can use multiple times)
      -d s, --define s      Define other params (can use multiple times)
      -f s, --force-dnssec-type s
                            Use DNSSEC type (one of: detect, unsigned, NSEC,
                            NSEC3) [detect]
      -e, --errors-only     Show validation errors only [False]
      -v, --verbose         Show detailed processing info [False]

    Use @filename to read some/all arguments from a file.

    Use -d's to define optional, module-specific parameters if desired (e.g. to tell
    'xfr' loader to use a specific source address, use "-d xfr_source=1.2.3.4").
    The optional parameters are listed under each loader and validator description
    in DEFINE lines, if available.  The "short form" of parameter names (i.e.
    without the module name prefix) may be used instead, if multiple modules define
    the same "short form" name (e.g. -d now=20160328120000 can be used, and will set
    both 'rrsig_missing' and 'rrsig_orphan' validators' rrsig_missing_now and
    rrsig_orphan_now values, respectively).

    By default, all tests are run.  Use -i's to explicitly specify desired tests,
    or -x's to eliminate undesired tests.

    The list of available loaders is:
    ---------------------------------------------------------------------------
    LOADER: file - Loads a zone from a file in AXFR-type or Bind host-type format.
        DEFINE: (file_)allow_include - Allow file to include other files (default=1)
        DEFINE: (file_)rdclass - Class of records to pull (default=IN)
    LOADER: xfr - Loads a zone by XFR from a name server.
        DEFINE: (xfr_)af - The address family to use, AF_INET or AF_INET6 (default=None)
        DEFINE: (xfr_)keyalgorithm - The TSIG algorithm to use, one of: HMAC-MD5.SIG-ALG.REG.INT. hmac-sha1. hmac-sha224. hmac-sha256. hmac-sha384. hmac-sha512. (default=HMAC-MD5.SIG-ALG.REG.INT.)
        DEFINE: (xfr_)keyname - The name of the TSIG to use (default=None)
        DEFINE: (xfr_)keyring - The TSIG keyring to use, a text dict of name->base64_secret e.g. "{'n1':'H477A900','n2':'K845CL21'}" (default=None)
        DEFINE: (xfr_)lifetime - Total seconds to wait for complete transfer (default=None)
        DEFINE: (xfr_)rdclass - Class of records to pull (default=IN)
        DEFINE: (xfr_)rdtype - Type of XFR to perform, AXFR or IXFR (default=AXFR)
        DEFINE: (xfr_)serial - SOA serial number to use as base for IXFR diff (default=0)
        DEFINE: (xfr_)source - Source address for the transfer (default=None)
        DEFINE: (xfr_)source_port - Source port for the transfer (default=0)
        DEFINE: (xfr_)timeout - Seconds to wait for each response message (default=5.0)
        DEFINE: (xfr_)use_udp - Use UDP for IXFRing (default=0)

    The list of available validators is:
    ---------------------------------------------------------------------------
    TEST: dnskey_bits (REC_TEST[DNSKEY]) - Checks DNSKEY flags and protocol.
    TEST: dnskey_origin (ZONE_TEST) - Checks for a ZSK at zone origin.
    TEST: dnssectype_ambiguous (ZONE_TEST) - Checks for existence of both NSEC and NSEC3 in the zone.
    TEST: ns_origin (ZONE_TEST) - Checks for at least one NS at zone origin.
    TEST: nsec3_chain (ZONE_TEST) - Checks for valid NSEC3 chain.
    TEST: nsec3_missing (RRSET_TEST) - Checks that all (non-NSEC3/RRSIG, non-delegated) RRSets are covered with an NSEC3.
    TEST: nsec3_orphan (REC_TEST[NSEC3]) - Checks for orphan or invalid-covers NSEC3s.
    TEST: nsec3param_origin (ZONE_TEST) - Checks for an NSEC3PARAM at zone origin for nsec3-type zones.
    TEST: nsec_chain (ZONE_TEST) - Checks for valid NSEC chain.
    TEST: nsec_missing (RRSET_TEST) - Checks that all (non-NSEC/RRSIG, non-delegated) RRSets are covered with an NSEC.
    TEST: nsec_orphan (REC_TEST[NSEC]) - Checks for orphan or invalid-covers NSECs.
    TEST: nsecx_ttls_match (REC_TEST[NSEC,NSEC3]) - Checks that NSECx TTL's match SOA's minimum.
    TEST: rrsig_covers (REC_TEST[RRSIG]) - Checks RRSIG's don't cover RRSIG's.
    TEST: rrsig_missing (RRSET_TEST) - Checks that all (non-RRSIG, non-delegated) RRSets are covered with an RRSIG.
        DEFINE: (rrsig_missing_)now - Time to use for validating RRSIG time windows, e.g. 20150101123000 (default=None)
        DEFINE: (rrsig_missing_)now_offset - Number of seconds to offset the "now" value, e.g. -86400) (default=None)
    TEST: rrsig_orphan (REC_TEST[RRSIG]) - Checks for orphan RRSIGs.
        DEFINE: (rrsig_orphan_)now - Time to use for validating RRSIG time windows, e.g. 20150101123000 (default=None)
        DEFINE: (rrsig_orphan_)now_offset - Number of seconds to offset the "now" value, e.g. -86400) (default=None)
    TEST: rrsig_signer_match (REC_TEST[RRSIG]) - Checks RRSIG signers match the zone.
    TEST: rrsig_time (REC_TEST[RRSIG]) - Checks RRSIG's inception <= expiration.
    TEST: rrsig_ttls_match (REC_TEST[RRSIG]) - Checks RRSIG TTL's match original and covered TTL's.
    TEST: soa_origin (ZONE_TEST) - Checks for an SOA at zone origin.
    TEST: soa_unique (ZONE_TEST) - Checks for a single SOA in the zone.

Sample Usage
''''''''''''

Let's say you want to validate and only see errors an NSEC3-style DNSSEC zone
called "example", from a file, and wish to run all available/applicable validations.
Since this will check RRSIG signatures, you'll need to add a few defines to properly
state the "now" time to use for two of the validators.  Assuming a bash-like shell::

    $ ZONE_FILE=$VIRTUAL_ENV/lib/python2.7/site-packages/dns_sprockets_lib/tests/data/rfc5155_example.

    $ TIME_NOW=20100101000000

    $ dns_sprockets -z example -l file -s $ZONE_FILE -e \
        -d rrsig_missing_now=$TIME_NOW -d rrsig_orphan_now=$TIME_NOW

    # dns_sprockets (1.0.0) - A DNS Zone validation tool
    # Checking zone: example.
    # Loader: file from: rfc5155_example. elapsed=0.018354 secs
    # Zone appears to be DNSSEC type: NSEC3
    # Extra defines: ['rrsig_missing_now=20100101000000', 'rrsig_orphan_now=20100101000000']
    # Skipping test: nsec_chain  (DNSSEC type for zone: NSEC3, for test: NSEC)
    # Skipping test: nsec_missing  (DNSSEC type for zone: NSEC3, for test: NSEC)
    # Skipping test: nsec_orphan  (DNSSEC type for zone: NSEC3, for test: NSEC)
    # Running tests: ['dnskey_origin', 'dnssectype_ambiguous', 'ns_origin', 'nsec3_chain', 'nsec3param_origin', 'soa_origin', 'soa_unique', 'nsec3_missing', 'rrsig_missing', 'dnskey_bits', 'nsec3_orphan', 'nsecx_ttls_match', 'rrsig_covers', 'rrsig_orphan', 'rrsig_signer_match', 'rrsig_time', 'rrsig_ttls_match']
    # END RESULT: 0 ERRORS in 229 tests
    # TOTAL ELAPSED TIME: 0.063526 SECS  LOAD TIME: 0.018354 SECS  TEST TIME: 0.045172 SECS

    $ echo $?
    0

*UPDATE*  New in version 1.1.8: Can now just specify "-d now=$TIME_NOW" as a
shortcut for "-d rrsig_missing_now=$TIME_NOW -d rrsig_orphan_now=$TIME_NOW"

OK, all tests passed, but that's not too interesting.  Let's repeat that, except
with a slightly modified zone file: one of the NSEC3's (and its associated RRSIG
record) has been removed::

    $ ZONE_FILE=$VIRTUAL_ENV/lib/python2.7/site-packages/dns_sprockets_lib/tests/data/rfc5155_example._nsec3_missing

    $ dns_sprockets -z example -l file -s $ZONE_FILE -e \
        -d rrsig_missing_now=$TIME_NOW -d rrsig_orphan_now=$TIME_NOW

    # dns_sprockets (1.0.0) - A DNS Zone validation tool
    # Checking zone: example.
    # Loader: file from: dns_sprockets_lib/tests/data/rfc5155_example._nsec3_missing elapsed=0.023993 secs
    # Zone appears to be DNSSEC type: NSEC3
    # Extra defines: ['rrsig_missing_now=20100101000000', 'rrsig_orphan_now=20100101000000']
    # Skipping test: nsec_chain  (DNSSEC type for zone: NSEC3, for test: NSEC)
    # Skipping test: nsec_missing  (DNSSEC type for zone: NSEC3, for test: NSEC)
    # Skipping test: nsec_orphan  (DNSSEC type for zone: NSEC3, for test: NSEC)
    # Running tests: ['dnskey_origin', 'dnssectype_ambiguous', 'ns_origin', 'nsec3_chain', 'nsec3param_origin', 'soa_origin', 'soa_unique', 'nsec3_missing', 'rrsig_missing', 'dnskey_bits', 'nsec3_orphan', 'nsecx_ttls_match', 'rrsig_covers', 'rrsig_orphan', 'rrsig_signer_match', 'rrsig_time', 'rrsig_ttls_match']
    TEST nsec3_chain(ZONE(example. IN)) => FAIL: Chain broken at R53BQ7CC2UVMUBFU5OCMM6PERS9TK9EN (next=T644EBQK9BIBCNA874GIVR6JOJ62MLHV doesn't exist)
    TEST nsec3_missing(RRSET(xx.example. IN A)) => FAIL: No NSEC3's found for name: t644ebqk9bibcna874givr6joj62mlhv.example.
    TEST nsec3_missing(RRSET(xx.example. IN HINFO)) => FAIL: No NSEC3's found for name: t644ebqk9bibcna874givr6joj62mlhv.example.
    TEST nsec3_missing(RRSET(xx.example. IN AAAA)) => FAIL: No NSEC3's found for name: t644ebqk9bibcna874givr6joj62mlhv.example.
    # END RESULT: 4 ERRORS in 221 tests
    # TOTAL ELAPSED TIME: 0.064603 SECS  LOAD TIME: 0.023993 SECS  TEST TIME: 0.040610 SECS 

    $ echo $?
    4

This time, we get errors from two validators.  The nsec3_chain validator issues a
"chain broken" error, and the nsec3_missing validator sees three RRSet's with the
same owner name that are "not covered" by the missing NSEC3.

Incidentally, these two data files (and others) are included in the package for
unit testing purposes, but can be useful to play with to see how dns_sprockets
reports various problems.

Return Codes
''''''''''''

The application returns a numerical value back to the user:

- **0** If there were no failed validations.
- **1-254** The number of failed validations, up to a limit of 254.
- **255** A special code for fatal exceptions.

TO-DO's
'''''''

The following is a non-exhaustive list of things to do (help anyone?):

- Respect the "opt-out" flag in NSEC3 records; right now, assuming none are opt-out.
- More loaders and (especially) validators!
- More real-world trials.

Developer Information
---------------------

This *long* section discusses dns_sprockets for those who may be interested in 
adding more loaders or validators.  If that's you, great!  Please consider 
contributing your work to the project, it is most welcome!  Especially welcome
are unit tests that accompany any new code!  (currently using Nose for testing).

Framework Architecture
''''''''''''''''''''''

Inspiration for this application comes from a similar tool written in Perl called
`donuts <http://www.dnssec-tools.org>`_.  It too uses the concept of plugins for
its validators.

This framework essentially revolves around the two types of plugins: Loaders and
validator plugins, which are stored in two project subfolders 
(dns_sprockets_lib/loaders and dns_sprockets_lib/validators, respectively).  At
runtime, the app scans both folders and makes their contents available for use::

    A note on the naming conventions: plugins are stored in files with
    underscore-style names (e.g. nsec3_chain.py) and are expected to
    contain a class that implements the plugin, with a camelcase-style
    name that corresponds to the file name (e.g. Nsec3Chain).

The main logic of the app resides in the DNSSprocketsImpl.run() method (in 
dns_sprockets_lib/dns_sprockets_impl.py).  Pseudo-code is:

- Scan zone loaders and load them into memory as Python classes.
- Create an instance of the specified zone loader.
- Scan validators and load them into memory as Python classes.
- Instantiate specified validators and categorize by validator type.
- Run the specified zone loader instance to obtain a dns.zone.Zone object.
- Construct a "Context" instance, initialized by the dns.zone.Zone object.
- Filter-out any validator instances that do not make sense for the DNSSEC type of the zone.
- Run the zone-type validators against the Context.
- Iterate Nodes in the zone object:
    - Run the node-type validators against the Context and Node.
    - Iterate RRSets in the Node:
        - Run the RRSet-type validators against the Context and RRSet.
        - Iterate Records in the RRSet:
            - Run the record-type validators against the Context and Record.

The use of the `dnspython <http://www.dnspython.org>`_ library pervades the 
application (so if you're familiar with it already, you've got an excellent start):
The loaders read from some source and return a dnspython dns.zone.Zone object to
the framework.  Similarly, the framework presents to the validators the same 
dns.zone.Zone object for examination.

Zone Loaders
''''''''''''

Zone loaders are classes derived from dns_sprockets_lib.loaders.ZoneLoader 
(in the dns_sprockets_lib/loaders/__init__.py file), which defines the interface
expected by the framework::

    class ZoneLoader(object):
        '''
        [Base class for zone loaders]
        '''
        LOADER_NAME = None  # Automatically set in __init__.
        LOADER_OPTARGS = {}  # Override possible!  e.g.: {'now': (None, 'Time to use for now')}

        def __init__(self, args):
            '''
            Ctor, caches the arguments used to run the application, and grabs any
            optional test arguments.
            '''
            self.LOADER_NAME = utils.camelcase_to_underscores(self.__class__.__name__)
            self.args = args

            utils.process_optargs(self.LOADER_OPTARGS, self.LOADER_NAME, self)

        def run(self):
            '''
            Runs the zone loader -- must override!

            :return: A dns.zone.Zone instance.
            '''
            pass

Two class variables are expected:

- **LOADER_NAME** Contains the underscore-style name of the loader, and is 
  automatically set up in the __init__() method.
- **LOADER_OPTARGS** Contains any plugin-specific parameters that may be set from
  the command-line *...more on this later*.

Two methods are expected:

- **__init__()** Takes the arguments object containing the command-line
  options passed by the user to the application.
- **run()** Invokes the zone loader functionality and returns a dns.zone.Zone 
  object.

As an example, the code for the File loader is show here.  It is almost trivial
because it takes advantage of the built-in host file loading available in the
dnspython library::

    class File(loaders.ZoneLoader):
        '''
        Loads a zone from a file in AXFR-type or Bind host-type format.
        '''
        LOADER_OPTARGS = {
            'rdclass': ('IN', 'Class of records to pull'),
            'allow_include': ('1', 'Allow file to include other files')}

        def __init__(self, args):
            '''
            Ctor.
            '''
            self.rdclass = None
            self.allow_include = None
            super(File, self).__init__(args)

        def run(self):
            '''
            :return: A dns.zone.Zone instance.
            '''
            other_args = {
                'origin': self.args.zone,
                'relativize': False,
                'filename': self.args.source,
                'check_origin': False,
                'rdclass': dns.rdataclass.from_text(self.rdclass),
                'allow_include': bool(int(self.allow_include))}

            return dns.zone.from_file(self.args.source, **other_args)

Please note the __init__() method calls back into the base class to include its
useful and necessary functionality!  Also be aware that the class docstring is 
used for the description of the loader, as shown in the --help output (keep it
brief!)

Validation Context
''''''''''''''''''

Once the framework obtains a dns.zone.Zone instance from the specified zone
loader, it constructs a Context instance from it, which is passed to the 
validators.  In addition to the application's command-line arguments (as
**context.args**) and the actual dns.zone.Zone instance created by the loader
(as **context.zone_obj**), it contains some other attributes for the convenience
of validators (code for the Context class can be found in the 
dns_sprockets_lib/validators/__init__.py file).  Some of these are useful to
some validators, but can be ignored if not useful:

- **context.node_names** Contains DNSSEC-ordered list of all node names present
  in the zone (*including* empty-non-terminal names implied by wildcard names).
- **context.soa_rdataset** Contains the zone's SOA RRSet.
- **context.dnskey_rdataset** Contains the zone's DNSKEY RRSet.
- **context.nsec3param_rdataset** Contains the zone's NSEC3PARAM RRSet.
- **context.delegated_names** Contains list of any delegated names in the zone.
- **context.dnssec_type** Indicates the DNSSEC type of the zone.

A method called **is_delegated()** is also available, which lets clients easily
determine if a given owner name is delegated.

Validators
''''''''''

Validators are classes *ultimately* derived from dns_sprockets_lib.validators._Validator 
(in the dns_sprockets_lib/validators/__init__.py file).  This is the base class
for the four more specialized validator classes (ZoneTest, NodeTest, RRSetTest, and
RecordTest)::

    class _Validator(object):
        '''
        [Base class for validator classes]
        '''
        TEST_NAME = None  # Automatically set in __init__.
        TEST_TYPE = None  # Override expected!  e.g.: ZONE_TEST
        TEST_DNSSECTYPE = None  # Override possible!  one of: None, True, 'NSEC' or 'NSEC3'
        TEST_RRTYPE = None  # Override possible!  e.g.: 'A', or 'RRSIG,NSEC3PARAM'
        TEST_OPTARGS = {}  # Override possible!  e.g.: {'now': (None, 'Time to use for now')}

        def __init__(self, args):
            '''
            Ctor, caches the arguments used to run the application, and grabs any
            optional test arguments.
            '''
            self.TEST_NAME = utils.camelcase_to_underscores(self.__class__.__name__)
            self.args = args

            utils.process_optargs(self.TEST_OPTARGS, self.TEST_NAME, self)

Five class variables are expected:

- **TEST_NAME** Contains the underscore-style name of the validator, and is 
  automatically set up in the __init__() method.
- **TEST_TYPE** Indicates the type of validator.
- **TEST_DNSSECTYPE** Indicates the DNSSEC-type of the validator.
- **TEST_RRTYPE** Indicates zero or more resource record types the validator is
  specialized for.  If no types specified, ALL types are assumed.
- **TEST_OPTARGS** Contains any plugin-specific parameters that may be set from
  the command-line *...more on this later*.

One method is provided:

- **__init__()** Convenince method for use by sub-classes.

There are four _Validator-derived classes for use by plugins (also defined in the
dns_sprockets_lib/validators/__init__.py file).  They provide slight convenience
by defining **TEST_TYPE** properly, but more importantly expose different 
**run()** signatures, specific to each type of validator::

    class ZoneTest(_Validator):
        '''
        [Base class for zone-type validators]
        '''
        TEST_TYPE = ZONE_TEST

        def run(self, suggested_tested, context):
            '''
            Runs the zone-type validator.

            :param str suggested_tested: A suggested tested value.
            :param obj context: The testing context.
            :return: A tuple (tested, result)
            '''
            return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME))


    class NodeTest(_Validator):
        '''
        [Base class for node-type validators.  Derived classes *may* be restricted
        to specific RRType's by specifying a TEST_RRTYPE]
        '''
        TEST_TYPE = NODE_TEST

        def run(self, context, suggested_tested, name, node):
            '''
            Runs the node-type validator.  If a TEST_RRTYPE specified, the node
            presented to the validator will be filtered accordingly.

            :param obj context: The testing context.
            :param str suggested_tested: A suggested tested value.
            :param str name: The name being tested.
            :param obj node: The dns.Node corresponding to the name.
            :return: A tuple (tested, result)
            '''
            return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME))


    class RRSetTest(_Validator):
        '''
        [Base class for rrset-type validators.  Derived classes *may* be restricted
        to specific RRType's by specifying a TEST_RRTYPE]
        '''
        TEST_TYPE = RRSET_TEST

        def run(self, context, suggested_tested, name, rdataset):
            '''
            Runs the name-type validator.  If a TEST_RRTYPE is specified, the RRSet
            presented to the validator will be filtered accordingly.

            :param obj context: The testing context.
            :param str suggested_tested: A suggested tested value.
            :param str name: The name being tested.
            :param obj rdataset: The dns.rdataset corresponding to the name.
            :return: A tuple (tested, result)
            '''
            return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME))


    class RecTest(_Validator):
        '''
        [Base class for record-type validators.  Derived classes *may* be restricted
        to specific RRType's by specifying a TEST_RRTYPE]
        '''
        TEST_TYPE = REC_TEST

        def run(self, context, suggested_tested, name, ttl, rdata):
            '''
            Runs the record-type validator.  If a TEST_RRTYPE is specified, the
            validator will only see those types of records.

            :param obj context: The testing context.
            :param str suggested_tested: A suggested tested value.
            :param str name: The name of the record being tested.
            :param int ttl: The TTL of the record being tested.
            :param obj rdata: The dns.rdata.Rdata object being tested.
            :return: A tuple (tested, result)
            '''
            return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME))

The **suggested_tested** string contains a default name of the object being tested,
be it a zone, node, RRSet or record.  It can be used in most instances as the first
item in the returned tuple from **run()**::

    Notes on the run() return tuple (tested, result): 

    - If a validation is skipped for whatever reason, the 'tested' 
      value should be None, which causes the framework to ignore the
      run.  Otherwise, a value describing the object being tested 
      should be set (and as mentioned 'suggested_tested' is a good 
      value).

    - The actual result of an un-skipped test is returned in 
      'result'.  If the test passes, simply return None.  Otherwise,
      return a string describing the failure.

As an example, the code for the RrsigTime validator is as follows.  The 
**TEST_DNSSECTYPE** is set to True to indicate the validation only makes sense 
for DNSSEC-type zones.  It is a record-type test, and only receives RRSIG records
due to the **TEST_RRTYPE** filtering applied.  The "context", "name" and "ttl" 
parameters are ignored for this validation.  The "rdata" parameter is used,
and is of type dns.rdata.Rdata (a type defined in dnspython)::

    class RrsigTime(validators.RecTest):
        '''
        Checks RRSIG's inception <= expiration.
        '''
        TEST_DNSSECTYPE = True
        TEST_RRTYPE = 'RRSIG'

        def run(self, context, suggested_tested, name, ttl, rdata):

            result = None
            if rdata.inception > rdata.expiration:
                result = 'Inception time greater than expiration time'
            return (suggested_tested, result)

Please note that if your validator needs to define an **__init__()** method,
it must call the base's **__init__()** to receive its useful and necessary
functionality!  Also be aware that the class docstring is used for the 
description of the validator, as shown in the --help output (keep it brief!)

Plugin-Specific Arguments
'''''''''''''''''''''''''

Loaders and validators may have parameters that are specific to themselves.
The framework's --define command-line switch is used to pass these parameters
to the plugins.  

The names of the --define parameters are of the form: <pluginname>_<paramname>
(e.g. "rrsig_missing_now" specifies the "now" parameter for the rrsig_missing
validator), and are translated and set as plugin attributes as <paramname> 
(e.g. self.now in rrsig_missing methods).

As an example (and shown earlier), the File zone loader plugin defines two 
parameters specific to loading zone files::

    LOADER_OPTARGS = {
        'rdclass': ('IN', 'Class of records to pull'),
        'allow_include': ('1', 'Allow file to include other files')}

**LOADER_OPTARGS** (and **TEST_OPTARGS** for validators) is a dictionary of 
parameter descriptors; each entry is keyed by <paramname>, and indexes a 2-tuple
of (<defaultvalue>, <description>).  If no --define for the parameter is passed,
the <defaultvalue> will be set.  The <description> is used for --help output, 
so keep it brief please!