Browse Source

[2437] supported python wrapper of checkZone().

JINMEI Tatuya 12 years ago
parent
commit
f1e9ef1f49

+ 1 - 0
src/lib/dns/python/Makefile.am

@@ -27,6 +27,7 @@ libb10_pydnspp_la_SOURCES += edns_python.cc edns_python.h
 libb10_pydnspp_la_SOURCES += message_python.cc message_python.h
 libb10_pydnspp_la_SOURCES += rrset_collection_python.cc
 libb10_pydnspp_la_SOURCES += rrset_collection_python.h
+libb10_pydnspp_la_SOURCES += zone_checker_python.cc zone_checker_python.h
 
 libb10_pydnspp_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
 libb10_pydnspp_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS)

+ 10 - 2
src/lib/dns/python/pydnspp.cc

@@ -57,6 +57,9 @@
 #include "tsig_python.h"
 #include "tsig_rdata_python.h"
 #include "tsigrecord_python.h"
+#include "zone_checker_python.h"
+
+#include "zone_checker_python_inc.cc"
 
 using namespace isc::dns;
 using namespace isc::dns::python;
@@ -729,6 +732,11 @@ initModulePart_TSIGRecord(PyObject* mod) {
     return (true);
 }
 
+PyMethodDef methods[] = {
+    { "check_zone", internal::pyCheckZone, METH_VARARGS, dns_checkZone_doc },
+    { NULL, NULL, 0, NULL }
+};
+
 PyModuleDef pydnspp = {
     { PyObject_HEAD_INIT(NULL) NULL, 0, NULL},
     "pydnspp",
@@ -738,13 +746,13 @@ PyModuleDef pydnspp = {
     "and OutputBuffer for instance), and others may be necessary, but "
     "were not up to now.",
     -1,
-    NULL,
+    methods,
     NULL,
     NULL,
     NULL,
     NULL
 };
-}
+} // unnamed namespace
 
 PyMODINIT_FUNC
 PyInit_pydnspp(void) {

+ 1 - 0
src/lib/dns/python/tests/Makefile.am

@@ -19,6 +19,7 @@ PYTESTS += tsig_rdata_python_test.py
 PYTESTS += tsigerror_python_test.py
 PYTESTS += tsigkey_python_test.py
 PYTESTS += tsigrecord_python_test.py
+PYTESTS += zone_checker_python_test.py
 
 EXTRA_DIST = $(PYTESTS)
 EXTRA_DIST += testutil.py

+ 178 - 0
src/lib/dns/python/tests/zone_checker_python_test.py

@@ -0,0 +1,178 @@
+# Copyright (C) 2013  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software 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 INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM 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.
+
+import unittest
+import sys
+from pydnspp import *
+
+# A separate exception class raised from some tests to see if it's propagated.
+class FakeException(Exception):
+    pass
+
+class ZoneCheckerTest(unittest.TestCase):
+    def __callback(self, reason, reasons):
+        # Issue callback for check_zone().  It simply records the given reason
+        # string in the given list.
+        reasons.append(reason)
+
+    def test_check(self):
+        errors = []
+        warns = []
+
+        # A successful case with no warning.
+        rrsets = RRsetCollection(b'example.org. 0 SOA . . 0 0 0 0 0\n' +
+                                 b'example.org. 0 NS ns.example.org.\n' +
+                                 b'ns.example.org. 0 A 192.0.2.1\n',
+                                 Name('example.org'), RRClass.IN())
+        self.assertTrue(check_zone(Name('example.org'), RRClass.IN(),
+                                   rrsets,
+                                   (lambda r: self.__callback(r, errors),
+                                    lambda r: self.__callback(r, warns))))
+        self.assertEqual([], errors)
+        self.assertEqual([], warns)
+
+        # Check fails and one additional warning.
+        rrsets = RRsetCollection(b'example.org. 0 NS ns.example.org.',
+                                 Name('example.org'), RRClass.IN())
+        self.assertFalse(check_zone(Name('example.org'), RRClass.IN(), rrsets,
+                                    (lambda r: self.__callback(r, errors),
+                                     lambda r: self.__callback(r, warns))))
+        self.assertEqual(['zone example.org/IN: has 0 SOA records'], errors)
+        self.assertEqual(['zone example.org/IN: NS has no address records ' +
+                          '(A or AAAA)'], warns)
+
+        # Same RRset collection, suppressing callbacks
+        errors = []
+        warns = []
+        self.assertFalse(check_zone(Name('example.org'), RRClass.IN(), rrsets,
+                                    (None, None)))
+        self.assertEqual([], errors)
+        self.assertEqual([], warns)
+
+    def test_check_badarg(self):
+        rrsets = RRsetCollection()
+        # Bad types
+        self.assertRaises(TypeError, check_zone, 1, RRClass.IN(), rrsets,
+                          (None, None))
+        self.assertRaises(TypeError, check_zone, Name('example'), 1, rrsets,
+                          (None, None))
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          1, (None, None))
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          rrsets, 1)
+
+        # Bad callbacks
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          rrsets, (None, None, None))
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          rrsets, (1, None))
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          rrsets, (None, 1))
+
+        # Extra/missing args
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          rrsets, (None, None), 1)
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          rrsets)
+        check_zone(Name('example'), RRClass.IN(), rrsets, (None, None))
+
+    def test_check_callback_fail(self):
+        # Let the call raise a Python exception.  It should be propagated to
+        # the top level.
+        def __bad_callback(reason):
+            raise FakeException('error in callback')
+
+        # Using an empty collection, triggering an error callback.
+        self.assertRaises(FakeException, check_zone, Name('example.org'),
+                          RRClass.IN(), RRsetCollection(),
+                          (__bad_callback, None))
+
+        # An unusual case: the callback is expected to return None, but if it
+        # returns an actual object it shouldn't cause leak inside the callback.
+        class RefChecker:
+            pass
+        def __callback(reason, checker):
+            return checker
+
+        ref_checker = RefChecker()
+        orig_refcnt = sys.getrefcount(ref_checker)
+        check_zone(Name('example.org'), RRClass.IN(), RRsetCollection(),
+                   (lambda r: __callback(r, ref_checker), None))
+        self.assertEqual(orig_refcnt, sys.getrefcount(ref_checker))
+
+    def test_check_custom_collection(self):
+        # Test if check_zone() works with pure-Python RRsetCollection.
+
+        class FakeRRsetCollection(RRsetCollectionBase):
+            # This is the Python-only collection class.  Its find() makes
+            # the check pass by default, by returning hardcoded RRsets.
+            # If raise_on_find is set to True, find() raises an exception.
+            # If find_result is set to something other than False, find()
+            # returns that specified value.
+
+            def __init__(self, raise_on_find=False, find_result=False):
+                self.__raise_on_find = raise_on_find
+                self.__find_result = find_result
+
+            def find(self, name, rrclass, rrtype):
+                if self.__raise_on_find:
+                    raise FakeException('find error')
+                if self.__find_result is not False:
+                    return self.__find_result
+                if rrtype == RRType.SOA():
+                    soa = RRset(Name('example'), RRClass.IN(), rrtype,
+                                RRTTL(0))
+                    soa.add_rdata(Rdata(RRType.SOA(), RRClass.IN(),
+                                        '. . 0 0 0 0 0'))
+                    return soa
+                if rrtype == RRType.NS():
+                    ns = RRset(Name('example'), RRClass.IN(), rrtype,
+                               RRTTL(0))
+                    ns.add_rdata(Rdata(RRType.NS(), RRClass.IN(),
+                                       'example.org'))
+                    return ns
+                return None
+
+        # A successful case.  Just checking it works in that case.
+        rrsets = FakeRRsetCollection()
+        self.assertTrue(check_zone(Name('example'), RRClass.IN(), rrsets,
+                                   (None, None)))
+
+        # Likewise, normal case but zone check fails.
+        rrsets = FakeRRsetCollection(False, None)
+        self.assertFalse(check_zone(Name('example'), RRClass.IN(), rrsets,
+                                    (None, None)))
+
+        # Our find() returns a bad type of result.
+        rrsets = FakeRRsetCollection(False, 1)
+        self.assertRaises(TypeError, check_zone, Name('example'), RRClass.IN(),
+                          rrsets, (None, None))
+
+        # Our find() returns an empty SOA RRset.  C++ zone checker code
+        # throws, which results in IscException.
+        rrsets = FakeRRsetCollection(False, RRset(Name('example'),
+                                                  RRClass.IN(),
+                                                  RRType.SOA(), RRTTL(0)))
+        self.assertRaises(IscException, check_zone, Name('example'),
+                          RRClass.IN(), rrsets, (None, None))
+
+        # Our find() raises an exception.  That exception is propagated to
+        # the top level.
+        rrsets = FakeRRsetCollection(True)
+        self.assertRaises(FakeException, check_zone, Name('example'),
+                          RRClass.IN(), rrsets, (None, None))
+
+if __name__ == '__main__':
+    unittest.main()

+ 224 - 0
src/lib/dns/python/zone_checker_python.cc

@@ -0,0 +1,224 @@
+// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software 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 ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC 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.
+
+// Enable this if you use s# variants with PyArg_ParseTuple(), see
+// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers
+#define PY_SSIZE_T_CLEAN
+
+// Python.h needs to be placed at the head of the program file, see:
+// http://docs.python.org/py3k/extending/extending.html#a-simple-example
+#include <Python.h>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <dns/python/name_python.h>
+#include <dns/python/rrclass_python.h>
+#include <dns/python/rrtype_python.h>
+#include <dns/python/rrset_python.h>
+#include <dns/python/rrset_collection_python.h>
+#include <dns/python/zone_checker_python.h>
+#include <dns/python/pydnspp_common.h>
+
+#include <exceptions/exceptions.h>
+
+#include <dns/name.h>
+#include <dns/rrclass.h>
+#include <dns/rrtype.h>
+#include <dns/rrset.h>
+#include <dns/rrset_collection_base.h>
+#include <dns/zone_checker.h>
+
+#include <boost/bind.hpp>
+
+#include <cstring>
+#include <string>
+#include <stdexcept>
+
+using std::string;
+using isc::util::python::PyObjectContainer;
+using namespace isc::dns;
+
+namespace {
+// This is a template for a common pattern of type mismatch error handling,
+// provided to save typing and repeating the mostly identical patterns.
+PyObject*
+setTypeError(PyObject* pobj, const char* var_name, const char* type_name) {
+    PyErr_Format(PyExc_TypeError, "%s must be a %s, not %.200s",
+                 var_name, type_name, pobj->ob_type->tp_name);
+    return (NULL);
+}
+}
+
+namespace isc {
+namespace dns {
+namespace python {
+namespace internal {
+
+namespace {
+// This is used to abort check_zone() and go back to the top level.
+// We use a separate exception so it won't be caught in the middle.
+class InternalException : public std::exception {
+};
+
+// This is a "wrapper" RRsetCollection subclass.  It's constructed with
+// a Python RRsetCollection object, and its find() calls the Python version
+// of RRsetCollection.find().  This way, the check_zone() wrapper will work
+// for pure-Python RRsetCollection classes, too.
+class PyRRsetCollection : public RRsetCollectionBase {
+public:
+    PyRRsetCollection(PyObject* po_rrsets) : po_rrsets_(po_rrsets) {}
+
+    virtual ConstRRsetPtr find(const Name& name, const RRClass& rrclass,
+                               const RRType& rrtype) const {
+        try {
+            // Convert C++ args to Python objects, and builds argument tuple
+            // to the Python method.  This should basically succeed.
+            PyObjectContainer poc_name(createNameObject(name));
+            PyObjectContainer poc_rrclass(createRRClassObject(rrclass));
+            PyObjectContainer poc_rrtype(createRRTypeObject(rrtype));
+            PyObjectContainer poc_args(Py_BuildValue("(OOOO)",
+                                                     po_rrsets_,
+                                                     poc_name.get(),
+                                                     poc_rrclass.get(),
+                                                     poc_rrtype.get()));
+
+            // Call the Python method.
+            // PyObject_CallMethod is dirty and requires mutable C-string for
+            // method name and arguments.  While it's unlikely for these to
+            // be modified, we err on the side of caution and make copies.
+            char method_name[sizeof("find")];
+            char method_args[sizeof("(OOO)")];
+            std::strcpy(method_name, "find");
+            std::strcpy(method_args, "(OOO)");
+            PyObjectContainer poc_result(
+                PyObject_CallMethod(po_rrsets_, method_name, method_args,
+                                    poc_name.get(), poc_rrclass.get(),
+                                    poc_rrtype.get()));
+            PyObject* const po_result = poc_result.get();
+            if (po_result == Py_None) {
+                return (ConstRRsetPtr());
+            } else if (PyRRset_Check(po_result)) {
+                return (PyRRset_ToRRsetPtr(po_result));
+            } else {
+                PyErr_SetString(PyExc_TypeError, "invalid type for "
+                                "RRsetCollection.find(): must be None "
+                                "or RRset");
+                throw InternalException();
+            }
+        } catch (const isc::util::python::PyCPPWrapperException& ex) {
+            // This normally means the method call fails.  Propagate the
+            // already-set Python error to the top level.  Other C++ exceptions
+            // are really unexpected, so we also (implicitly) propagate it
+            // to the top level and recognize it as "unexpected failure".
+            throw InternalException();
+        }
+    }
+
+    virtual IterPtr getBeginning() {
+        isc_throw(NotImplemented, "iterator support is not yet available");
+    }
+    virtual IterPtr getEnd() {
+        isc_throw(NotImplemented, "iterator support is not yet available");
+    }
+
+private:
+    PyObject* const po_rrsets_;
+};
+
+void
+callback(const string& reason, PyObject* obj) {
+    PyObjectContainer poc_args(Py_BuildValue("(s#)", reason.c_str(),
+                                             reason.size()));
+    PyObject* po_result = PyObject_CallObject(obj, poc_args.get());
+    if (po_result == NULL) {
+        throw InternalException();
+    }
+    Py_DECREF(po_result);
+}
+
+ZoneCheckerCallbacks::IssueCallback
+PyCallable_ToCallback(PyObject* obj) {
+    if (obj == Py_None) {
+        return (NULL);
+    }
+    return (boost::bind(callback, _1, obj));
+}
+
+}
+
+PyObject*
+pyCheckZone(PyObject*, PyObject* args) {
+    try {
+        PyObject* po_name;
+        PyObject* po_rrclass;
+        PyObject* po_rrsets;
+        PyObject* po_error;
+        PyObject* po_warn;
+
+        if (PyArg_ParseTuple(args, "OOO(OO)", &po_name, &po_rrclass,
+                             &po_rrsets, &po_error, &po_warn)) {
+            if (!PyName_Check(po_name)) {
+                return (setTypeError(po_name, "zone_name", "Name"));
+            }
+            if (!PyRRClass_Check(po_rrclass)) {
+                return (setTypeError(po_rrclass, "zone_rrclass", "RRClass"));
+            }
+            if (!PyObject_TypeCheck(po_rrsets, &rrset_collection_base_type)) {
+                return (setTypeError(po_rrsets, "zone_rrsets",
+                                     "RRsetCollectionBase"));
+            }
+            if (po_error != Py_None && PyCallable_Check(po_error) == 0) {
+                return (setTypeError(po_error, "error", "callable or None"));
+            }
+            if (po_warn != Py_None && PyCallable_Check(po_warn) == 0) {
+                return (setTypeError(po_warn, "warn", "callable or None"));
+            }
+
+            PyRRsetCollection py_rrsets(po_rrsets);
+            if (checkZone(PyName_ToName(po_name),
+                          PyRRClass_ToRRClass(po_rrclass), py_rrsets,
+                          ZoneCheckerCallbacks(
+                              PyCallable_ToCallback(po_error),
+                              PyCallable_ToCallback(po_warn)))) {
+                Py_RETURN_TRUE;
+            } else {
+                Py_RETURN_FALSE;
+            }
+        }
+    } catch (const InternalException& ex) {
+        // Normally, error string should have been set already.  For some
+        // rare cases such as memory allocation failure, we set the last-resort
+        // error string.
+        if (PyErr_Occurred() == NULL) {
+            PyErr_SetString(PyExc_SystemError,
+                            "Unexpected failure in check_zone()");
+        }
+        return (NULL);
+    } catch (const std::exception& ex) {
+        const string ex_what = "Unexpected failure in check_zone(): " +
+            string(ex.what());
+        PyErr_SetString(po_IscException, ex_what.c_str());
+        return (NULL);
+    } catch (...) {
+        PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception");
+        return (NULL);
+    }
+
+    return (NULL);
+}
+
+} // namespace internal
+} // namespace python
+} // namespace dns
+} // namespace isc

+ 35 - 0
src/lib/dns/python/zone_checker_python.h

@@ -0,0 +1,35 @@
+// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software 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 ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC 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.
+
+#ifndef PYTHON_ZONE_CHECKER_H
+#define PYTHON_ZONE_CHECKER_H 1
+
+#include <Python.h>
+
+namespace isc {
+namespace dns {
+namespace python {
+namespace internal {
+
+PyObject* pyCheckZone(PyObject* self, PyObject* args);
+
+} // namespace python
+} // namespace python
+} // namespace dns
+} // namespace isc
+#endif // PYTHON_ZONE_CHECKER_H
+
+// Local Variables:
+// mode: c++
+// End:

+ 79 - 0
src/lib/dns/python/zone_checker_python_inc.cc

@@ -0,0 +1,79 @@
+namespace {
+// Modifications
+//   - callbacks => (error, warn)
+//   - recover paragraph before itemization (it's a bug of convert script)
+//   - correct broken format for nested items (another bug of script)
+//   - true/false => True/False
+//   - removed Exception section (for simplicity)
+const char* const dns_checkZone_doc = "\
+check_zone(zone_name, zone_class, zone_rrsets, (error, warn)) -> bool\n\
+\n\
+Perform basic integrity checks on zone RRsets.\n\
+\n\
+This function performs some lightweight checks on zone's SOA and\n\
+(apex) NS records. Here, lightweight means it doesn't require\n\
+traversing the entire zone, and should be expected to complete\n\
+reasonably quickly regardless of the size of the zone.\n\
+\n\
+It distinguishes \"critical\" errors and other undesirable issues: the\n\
+former should be interpreted as the resulting zone shouldn't be used\n\
+further, e.g, by an authoritative server implementation; the latter\n\
+means the issues are better to be addressed but are not necessarily\n\
+considered to make the zone invalid. Critical errors are reported via\n\
+the error() function, and non critical issues are reported via warn().\n\
+\n\
+Specific checks performed by this function is as follows.  Failure of\n\
+a check is considered a critical error unless noted otherwise:\n\
+\n\
+- There is exactly one SOA RR at the zone apex.\n\
+- There is at least one NS RR at the zone apex.\n\
+- For each apex NS record, if the NS name (the RDATA of the record) is\n\
+  in the zone (i.e., it's a subdomain of the zone origin and above any\n\
+  zone cut due to delegation), check the following:\n\
+  - the NS name should have an address record (AAAA or A). Failure of\n\
+    this check is considered a non critical issue.\n\
+  - the NS name does not have a CNAME. This is prohibited by Section\n\
+    10.3 of RFC 2181.\n\
+  - the NS name is not subject to DNAME substitution. This is prohibited\n\
+    by Section 4 of RFC 6672.\n\
+\n\
+In addition, when the check is completed without any\n\
+critical error, this function guarantees that RRsets for the SOA and\n\
+(apex) NS stored in the passed RRset collection have the expected\n\
+type of Rdata objects, i.e., generic.SOA and generic.NS,\n\
+respectively. (This is normally expected to be the case, but not\n\
+guaranteed by the API).\n\
+\n\
+As for the check on the existence of AAAA or A records for NS names,\n\
+it should be noted that BIND 9 treats this as a critical error. It's\n\
+not clear whether it's an implementation dependent behavior or based\n\
+on the protocol standard (it looks like the former), but to make it\n\
+sure we need to confirm there is even no wildcard match for the names.\n\
+This should be a very rare configuration, and more expensive to\n\
+detect, so we do not check this condition, and treat this case as a\n\
+non critical issue.\n\
+\n\
+This function indicates the result of the checks (whether there is a\n\
+critical error) via the return value: It returns True if there is no\n\
+critical error and returns False otherwise. It doesn't throw an\n\
+exception on encountering an error so that it can report as many\n\
+errors as possible in a single call. If an exception is a better way\n\
+to signal the error, the caller can pass a callable object as error()\n\
+that throws.\n\
+\n\
+This function can still throw an exception if it finds a really bogus\n\
+condition that is most likely to be an implementation bug of the\n\
+caller. Such cases include when an RRset contained in the RRset\n\
+collection is empty.\n\
+\n\
+Parameters:\n\
+  zone_name  The name of the zone to be checked\n\
+  zone_class The RR class of the zone to be checked\n\
+  zone_rrsets The collection of RRsets of the zone\n\
+  error      Callable object used to report errors\n\
+  warn       Callable object used to report non-critical issues\n\
+\n\
+Return Value(s): True if no critical errors are found; False\n\
+otherwise.\n\
+";
+} // unnamed namespace