Commit f1e9ef1f authored by JINMEI Tatuya's avatar JINMEI Tatuya

[2437] supported python wrapper of checkZone().

parent 96e05eca
......@@ -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)
......
......@@ -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) {
......
......@@ -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
......
# 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()
// 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
// 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:
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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment