Browse Source

[trac983] added bindings for RequestLoader as discussed in review.
the only exposed interface is load() right now.
also made overall document cleanup/enhancements.

JINMEI Tatuya 14 years ago
parent
commit
d0df4daafe

+ 2 - 0
src/lib/python/isc/acl/Makefile.am

@@ -17,6 +17,7 @@ acl_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS)
 
 dns_la_SOURCES = dns.h dns.cc dns_requestacl_python.h dns_requestacl_python.cc
 dns_la_SOURCES += dns_requestcontext_python.h dns_requestcontext_python.cc
+dns_la_SOURCES += dns_requestloader_python.h dns_requestloader_python.cc
 dns_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
 dns_la_LDFLAGS = $(PYTHON_LDFLAGS)
 # Note: PYTHON_CXXFLAGS may have some -Wno... workaround, which must be
@@ -34,6 +35,7 @@ dns_la_LIBADD = $(top_builddir)/src/lib/acl/libdnsacl.la
 dns_la_LIBADD += $(PYTHON_LIB)
 
 EXTRA_DIST = acl.py dns.py
+EXTRA_DIST += acl_inc.cc
 EXTRA_DIST += dnsacl_inc.cc dns_requestacl_inc.cc dns_requestcontext_inc.cc
 
 CLEANDIRS = __pycache__

+ 3 - 2
src/lib/python/isc/acl/acl.cc

@@ -20,6 +20,8 @@
 
 using namespace isc::util::python;
 
+#include "acl_inc.cc"
+
 namespace {
 // Commonly used Python exception objects.  Right now the acl module consists
 // of only one .cc file, so we hide them in an unnamed namespace.  If and when
@@ -34,8 +36,7 @@ namespace {
 PyModuleDef acl = {
     { PyObject_HEAD_INIT(NULL) NULL, 0, NULL},
     "isc.acl.acl",
-    "This module provides Python bindings for the C++ classes in the "
-    "isc::acl namespace",
+    acl_doc,
     -1,
     NULL,
     NULL,

+ 16 - 0
src/lib/python/isc/acl/acl_inc.cc

@@ -0,0 +1,16 @@
+namespace {
+const char* const acl_doc = "\
+Implementation module for ACL operations\n\n\
+This module provides Python bindings for the C++ classes in the\n\
+isc::acl namespace.\n\
+\n\
+Integer constants:\n\
+\n\
+ACCEPT, REJECT, DROP -- Default actions an ACL could perform.\n\
+  These are the commonly used actions in specific ACLs.\n\
+  It is possible to specify any other values, as the ACL class does\n\
+  nothing about them, but these look reasonable, so they are provided\n\
+  for convenience. It is not specified what exactly these mean and it's\n\
+  up to whoever uses them.\n\
+";
+} // unnamed namespace

+ 34 - 31
src/lib/python/isc/acl/dns.cc

@@ -27,6 +27,7 @@
 #include "dns.h"
 #include "dns_requestcontext_python.h"
 #include "dns_requestacl_python.h"
+#include "dns_requestloader_python.h"
 
 using namespace std;
 using boost::shared_ptr;
@@ -38,45 +39,21 @@ using namespace isc::acl::dns::python;
 #include "dnsacl_inc.cc"
 
 namespace {
-PyObject*
-loadRequestACL(PyObject*, PyObject* args) {
-    const char* acl_config;
-
-    if (PyArg_ParseTuple(args, "s", &acl_config)) {
-        try {
-            shared_ptr<RequestACL> acl(
-                getRequestLoader().load(Element::fromJSON(acl_config)));
-            s_RequestACL* py_acl = static_cast<s_RequestACL*>(
-                requestacl_type.tp_alloc(&requestacl_type, 0));
-            if (py_acl != NULL) {
-                py_acl->cppobj = acl;
-            }
-            return (py_acl);
-        } catch (const exception& ex) {
-            PyErr_SetString(getACLException("LoaderError"), ex.what());
-            return (NULL);
-        } catch (...) {
-            PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception");
-            return (NULL);
-        }
-    }
-
-    return (NULL);
-}
+// This is a Python binding object corresponding to the singleton loader used
+// in the C++ version of the library.
+// We can define it as a pure object rather than through an accessor function,
+// because in Python we can ensure it has been created and initialized
+// in the module initializer by the time it's actually used.
+s_RequestLoader* po_REQUEST_LOADER;
 
 PyMethodDef methods[] = {
-    { "load_request_acl", loadRequestACL, METH_VARARGS, load_request_acl_doc },
     { NULL, NULL, 0, NULL }
 };
 
 PyModuleDef dnsacl = {
     { PyObject_HEAD_INIT(NULL) NULL, 0, NULL},
     "isc.acl.dns",
-    "This module provides Python bindings for the C++ classes in the "
-    "isc::acl::dns namespace.  Specifically, it defines Python interfaces of "
-    "handling access control lists (ACLs) with DNS related contexts.\n\n"
-    "These bindings are close match to the C++ API, but they are not complete "
-    "(some parts are not needed) and some are done in more python-like ways.",
+    dnsacl_doc,
     -1,
     methods,
     NULL,
@@ -127,6 +104,32 @@ PyInit_dns(void) {
         Py_DECREF(mod);
         return (NULL);
     }
+    if (!initModulePart_RequestLoader(mod)) {
+        Py_DECREF(mod);
+        return (NULL);
+    }
+
+    // Module constants
+    try {
+        if (po_REQUEST_LOADER == NULL) {
+            po_REQUEST_LOADER = static_cast<s_RequestLoader*>(
+                requestloader_type.tp_alloc(&requestloader_type, 0));
+        }
+        if (po_REQUEST_LOADER != NULL) {
+            // We gain and keep our own reference to the singleton object
+            // for the same reason as that for exception objects (see comments
+            // in pycppwrapper_util for more details).  Note also that we don't
+            // bother to release the reference even if exception is thrown
+            // below (in fact, we cannot delete the singleton loader).
+            po_REQUEST_LOADER->cppobj = &getRequestLoader();
+            Py_INCREF(po_REQUEST_LOADER);
+        }
+        PyObjectContainer(po_REQUEST_LOADER).installToModule(mod,
+                                                             "REQUEST_LOADER");
+    } catch (...) {
+        Py_DECREF(mod);
+        return (NULL);
+    }
 
     return (mod);
 }

+ 2 - 2
src/lib/python/isc/acl/dns_requestacl_inc.cc

@@ -4,7 +4,7 @@ The DNS Request ACL.\n\
 \n\
 It holds bunch of ordered entries, each one consisting of a check for\n\
 a given DNS Request context and an action, which is one of ACCEPT,\n\
-REJECT, or DROP, as defined in the isc.acl module.\n\
+REJECT, or DROP, as defined in the isc.acl.acl module.\n\
 The checks are tested in the order and first match counts.\n\
 \n\
 A RequestACL object cannot be constructed directly; an application\n\
@@ -16,7 +16,7 @@ const char* const RequestACL_execute_doc = "\
 execute(context) -> action \n\
 \n\
 The returned action is one of ACCEPT, REJECT or DROP as defined in\n\
-the isc.acl module.\n\
+the isc.acl.acl module.\n\
 \n\
 This is the function that takes the ACL entries one by one, checks the\n\
 context against conditions and if it matches, returns the action that\n\

+ 84 - 0
src/lib/python/isc/acl/dns_requestloader_inc.cc

@@ -0,0 +1,84 @@
+namespace {
+const char* const RequestLoader_doc = "\
+Loader of DNS Request ACLs.\n\
+\n\
+The goal of this class is to convert JSON description of an ACL to\n\
+object of the ACL class (including the checks inside it).\n\
+\n\
+The class can be used to load the checks only. This is supposed to be\n\
+used by compound checks to create the subexpressions.\n\
+\n\
+To allow any kind of checks to exist in the application, creators are\n\
+registered for the names of the checks (this feature is not yet\n\
+available for the python API).\n\
+\n\
+An ACL definition looks like this:  [\n\
+   {\n\
+      \"action\": \"ACCEPT\",\n\
+      \"match-type\": <parameter>\n\
+   },\n\
+   {\n\
+      \"action\": \"REJECT\",\n\
+      \"match-type\": <parameter>,\n\
+      \"another-match-type\": [<parameter1>, <parameter2>]\n\
+   },\n\
+   {\n\
+      \"action\": \"DROP\"\n\
+   }\n\
+ ]\n\
+ \n\
+\n\
+This is a list of elements. Each element must have an \"action\"\n\
+entry/keyword. That one specifies which action is returned if this\n\
+element matches (the value of the key is passed to the action loader\n\
+(see the constructor), which is one of ACCEPT,\n\
+REJECT, or DROP, as defined in the isc.acl.acl module.\n\
+\n\
+The rest of the element are matches. The left side is the name of the\n\
+match type (for example \"from\" to match for source IP address).\n\
+The <parameter> is whatever is needed to describe the\n\
+match and depends on the match type, the loader passes it verbatim to\n\
+creator of that match type.\n\
+\n\
+There may be multiple match types in single element. In such case, all\n\
+of the matches must match for the element to take action (so, in the\n\
+second element, both \"match-type\" and \"another-match-type\" must be\n\
+satisfied). If there's no match in the element, the action is\n\
+taken/returned without conditions, every time (makes sense as the last\n\
+entry, as the ACL will never get past it).\n\
+\n\
+The second entry shows another thing - if there's a list as the value\n\
+for some match and the match itself is not expecting a list, it is\n\
+taken as an \"or\" - a match for at last one of the choices in the\n\
+list must match. So, for the second entry, both \"match-type\" and\n\
+\"another-match-type\" must be satisfied, but the another one is\n\
+satisfied by either parameter1 or parameter2.\n\
+\n\
+Currently, a RequestLoader object cannot be constructed directly;\n\
+an application must use the singleton loader defined in the\n\
+isc.acl.dns module, i.e., isc.acl.dns.REQUEST_LOADER.\n\
+A future version of this implementation may be extended to give\n\
+applications full flexibility of creating arbitrary loader, when\n\
+this restriction may be removed.\n\
+";
+
+const char* const RequestLoader_load_doc = "\
+load(description) -> RequestACL\n\
+\n\
+Load a DNS ACL.\n\
+\n\
+This parses an ACL list, creates internal data for each rule\n\
+and returns a RequestACl object that contains all given rules.\n\
+\n\
+Exceptions:\n\
+  LoaderError Load failed.  The most likely cause of this is a syntax\n\
+              error in the description.  Other internal errors such as\n\
+              memory allocation failure is also converted to this\n\
+              exception.\n\
+\n\
+Parameters:\n\
+  description String representation of the JSON list of ACL.\n\
+\n\
+Return Value(s): The newly created RequestACL object\n\
+";
+} // unnamed namespace

+ 201 - 0
src/lib/python/isc/acl/dns_requestloader_python.cc

@@ -0,0 +1,201 @@
+// Copyright (C) 2011  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 <string>
+#include <stdexcept>
+
+#include <boost/shared_ptr.hpp>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <cc/data.h>
+
+#include <acl/dns.h>
+
+#include "dns.h"
+#include "dns_requestacl_python.h"
+#include "dns_requestloader_python.h"
+
+using namespace std;
+using boost::shared_ptr;
+using namespace isc::util::python;
+using namespace isc::data;
+using namespace isc::acl::dns;
+using namespace isc::acl::dns::python;
+
+//
+// Definition of the classes
+//
+
+// For each class, we need a struct, a helper functions (init, destroy,
+// and static wrappers around the methods we export), a list of methods,
+// and a type description
+
+//
+// RequestLoader
+//
+
+// Trivial constructor.
+s_RequestLoader::s_RequestLoader() : cppobj(NULL) {
+}
+
+// Import pydoc text
+#include "dns_requestloader_inc.cc"
+
+namespace {
+//
+// We declare the functions here, the definitions are below
+// the type definition of the object, since both can use the other
+//
+
+int
+RequestLoader_init(PyObject*, PyObject*, PyObject*) {
+    PyErr_SetString(getACLException("Error"),
+                    "RequestLoader cannot be directly constructed");
+    return (-1);
+}
+
+void
+RequestLoader_destroy(PyObject* po_self) {
+    s_RequestLoader* const self = static_cast<s_RequestLoader*>(po_self);
+    delete self->cppobj;
+    self->cppobj = NULL;
+    Py_TYPE(self)->tp_free(self);
+}
+
+PyObject*
+RequestLoader_load(PyObject* po_self, PyObject* args) {
+    s_RequestLoader* const self = static_cast<s_RequestLoader*>(po_self);
+    const char* acl_config;
+
+    if (PyArg_ParseTuple(args, "s", &acl_config)) {
+        try {
+            shared_ptr<RequestACL> acl(
+                self->cppobj->load(Element::fromJSON(acl_config)));
+            s_RequestACL* py_acl = static_cast<s_RequestACL*>(
+                requestacl_type.tp_alloc(&requestacl_type, 0));
+            if (py_acl != NULL) {
+                py_acl->cppobj = acl;
+            }
+            return (py_acl);
+        } catch (const exception& ex) {
+            PyErr_SetString(getACLException("LoaderError"), ex.what());
+            return (NULL);
+        } catch (...) {
+            PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception");
+            return (NULL);
+        }
+    }
+
+    return (NULL);
+}
+
+// This list contains the actual set of functions we have in
+// python. Each entry has
+// 1. Python method name
+// 2. Our static function here
+// 3. Argument type
+// 4. Documentation
+PyMethodDef RequestLoader_methods[] = {
+    { "load", RequestLoader_load, METH_VARARGS, RequestLoader_load_doc },
+    { NULL, NULL, 0, NULL }
+};
+} // end of unnamed namespace
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+// This defines the complete type for reflection in python and
+// parsing of PyObject* to s_RequestLoader
+// Most of the functions are not actually implemented and NULL here.
+PyTypeObject requestloader_type = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    "isc.acl.dns.RequestLoader",
+    sizeof(s_RequestLoader),                 // tp_basicsize
+    0,                                  // tp_itemsize
+    RequestLoader_destroy,       // tp_dealloc
+    NULL,                               // tp_print
+    NULL,                               // tp_getattr
+    NULL,                               // tp_setattr
+    NULL,                               // tp_reserved
+    NULL,                               // tp_repr
+    NULL,                               // tp_as_number
+    NULL,                               // tp_as_sequence
+    NULL,                               // tp_as_mapping
+    NULL,                               // tp_hash
+    NULL,                               // tp_call
+    NULL,                       // tp_str
+    NULL,                               // tp_getattro
+    NULL,                               // tp_setattro
+    NULL,                               // tp_as_buffer
+    Py_TPFLAGS_DEFAULT,                 // tp_flags
+    RequestLoader_doc,
+    NULL,                               // tp_traverse
+    NULL,                               // tp_clear
+    NULL, // tp_richcompare
+    0,                                  // tp_weaklistoffset
+    NULL,                               // tp_iter
+    NULL,                               // tp_iternext
+    RequestLoader_methods,                   // tp_methods
+    NULL,                               // tp_members
+    NULL,                               // tp_getset
+    NULL,                               // tp_base
+    NULL,                               // tp_dict
+    NULL,                               // tp_descr_get
+    NULL,                               // tp_descr_set
+    0,                                  // tp_dictoffset
+    RequestLoader_init,            // tp_init
+    NULL,                               // tp_alloc
+    PyType_GenericNew,                  // tp_new
+    NULL,                               // tp_free
+    NULL,                               // tp_is_gc
+    NULL,                               // tp_bases
+    NULL,                               // tp_mro
+    NULL,                               // tp_cache
+    NULL,                               // tp_subclasses
+    NULL,                               // tp_weaklist
+    NULL,                               // tp_del
+    0                                   // tp_version_tag
+};
+
+bool
+initModulePart_RequestLoader(PyObject* mod) {
+    // We initialize the static description object with PyType_Ready(),
+    // then add it to the module. This is not just a check! (leaving
+    // this out results in segmentation faults)
+    if (PyType_Ready(&requestloader_type) < 0) {
+        return (false);
+    }
+    void* p = &requestloader_type;
+    if (PyModule_AddObject(mod, "RequestLoader",
+                           static_cast<PyObject*>(p)) < 0) {
+        return (false);
+    }
+    Py_INCREF(&requestloader_type);
+
+    return (true);
+}
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc

+ 46 - 0
src/lib/python/isc/acl/dns_requestloader_python.h

@@ -0,0 +1,46 @@
+// Copyright (C) 2011  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_REQUESTLOADER_H
+#define __PYTHON_REQUESTLOADER_H 1
+
+#include <Python.h>
+
+#include <acl/dns.h>
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+
+// The s_* Class simply covers one instantiation of the object
+class s_RequestLoader : public PyObject {
+public:
+    s_RequestLoader();
+    RequestLoader* cppobj;
+};
+
+extern PyTypeObject requestloader_type;
+
+bool initModulePart_RequestLoader(PyObject* mod);
+
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc
+#endif // __PYTHON_REQUESTLOADER_H
+
+// Local Variables:
+// mode: c++
+// End:

+ 12 - 16
src/lib/python/isc/acl/dnsacl_inc.cc

@@ -1,21 +1,17 @@
 namespace {
-const char* const load_request_acl_doc = "\
-load_request_acl(description) -> RequestACL\n\
+const char* const dnsacl_doc = "\
+Implementation module for DNS ACL operations\n\n\
+This module provides Python bindings for the C++ classes in the\n\
+isc::acl::dns namespace.  Specifically, it defines Python interfaces of\n\
+handling access control lists (ACLs) with DNS related contexts.\n\
+These bindings are close match to the C++ API, but they are not complete\n\
+(some parts are not needed) and some are done in more python-like ways.\n\
 \n\
-Load a DNS ACL.\n\
+Special objects:\n\
 \n\
-This parses an ACL list, creates internal data for each rule\n\
-and returns a RequestACl object that contains all given rules.\n\
-\n\
-Exceptions:\n\
-  LoaderError Load failed.  The most likely cause of this is a syntax\n\
-              error in the description.  Other internal errors such as\n\
-              memory allocation failure is also converted to this\n\
-              exception.\n\
-\n\
-Parameters:\n\
-  description String representation of the JSON list of ACL.\n\
-\n\
-Return Value(s): The newly created RequestACL object\n\
+REQUEST_LOADER -- A singleton loader of ACLs. It is expected applications\n\
+  will use this function instead of creating their own loaders, because\n\
+  one is enough, this one will have registered default checks and it is\n\
+  known one, so any plugins can registrer additional checks as well.\n\
 ";
 } // unnamed namespace

+ 47 - 39
src/lib/python/isc/acl/tests/dns_test.py

@@ -29,7 +29,8 @@ def get_acl(prefix):
     that accepts addresses for the given IP prefix (and reject any others
     by default)
     '''
-    return load_request_acl('[{"action": "ACCEPT", "from": "' + prefix + '"}]')
+    return REQUEST_LOADER.load('[{"action": "ACCEPT", "from": "' + \
+                                   prefix + '"}]')
 
 def get_context(address):
     '''This is a simple shortcut wrapper for creating a RequestContext
@@ -98,64 +99,64 @@ class RequestACLTest(unittest.TestCase):
 
     def test_request_loader(self):
         # these shouldn't raise an exception
-        load_request_acl('[{"action": "DROP"}]')
-        load_request_acl('[{"action": "DROP", "from": "192.0.2.1"}]')
+        REQUEST_LOADER.load('[{"action": "DROP"}]')
+        REQUEST_LOADER.load('[{"action": "DROP", "from": "192.0.2.1"}]')
 
         # Invalid types
-        self.assertRaises(TypeError, load_request_acl, 1)
-        self.assertRaises(TypeError, load_request_acl, [])
+        self.assertRaises(TypeError, REQUEST_LOADER.load, 1)
+        self.assertRaises(TypeError, REQUEST_LOADER.load, [])
 
         # Incorrect number of arguments
-        self.assertRaises(TypeError, load_request_acl,
+        self.assertRaises(TypeError, REQUEST_LOADER.load,
                           '[{"action": "DROP"}]', 0)
 
     def test_bad_acl_syntax(self):
         # the following are derived from loader_test.cc
-        self.assertRaises(LoaderError, load_request_acl, '{}');
-        self.assertRaises(LoaderError, load_request_acl, '42');
-        self.assertRaises(LoaderError, load_request_acl, 'true');
-        self.assertRaises(LoaderError, load_request_acl, 'null');
-        self.assertRaises(LoaderError, load_request_acl, '"hello"');
-        self.assertRaises(LoaderError, load_request_acl, '[42]');
-        self.assertRaises(LoaderError, load_request_acl, '["hello"]');
-        self.assertRaises(LoaderError, load_request_acl, '[[]]');
-        self.assertRaises(LoaderError, load_request_acl, '[true]');
-        self.assertRaises(LoaderError, load_request_acl, '[null]');
-        self.assertRaises(LoaderError, load_request_acl, '[{}]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '{}');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '42');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, 'true');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, 'null');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '"hello"');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[42]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '["hello"]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[[]]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[true]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[null]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[{}]');
 
         # the following are derived from dns_test.cc
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "ACCEPT", "bad": "192.0.2.1"}]')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "ACCEPT", "from": 4}]')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "ACCEPT", "from": []}]')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "ACCEPT", "from": "bad"}]')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "ACCEPT", "from": null}]')
 
     def test_bad_acl_ipsyntax(self):
         # this test is derived from ip_check_unittest.cc
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "192.0.2.43/-1"}]')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "192.0.2.43//1"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "192.0.2.43/1/"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "/192.0.2.43/1"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "2001:db8::/xxxx"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "2001:db8::/32/s"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "1/"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "/1"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "192.0.2.0/33"')
-        self.assertRaises(LoaderError, load_request_acl,
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
                           '[{"action": "DROP", "from": "::1/129"')
 
     def test_execute(self):
@@ -174,13 +175,13 @@ class RequestACLTest(unittest.TestCase):
         self.assertEqual(REJECT, get_acl('32.1.13.184').execute(CONTEXT6))
 
         # A bit more complicated example, derived from resolver_config_unittest
-        acl = load_request_acl('[ {"action": "ACCEPT", ' +
-                               '     "from": "192.0.2.1"},' +
-                               '    {"action": "REJECT",' +
-                               '     "from": "192.0.2.0/24"},' +
-                               '    {"action": "DROP",' +
-                               '     "from": "2001:db8::1"},' +
-                               '] }')
+        acl = REQUEST_LOADER.load('[ {"action": "ACCEPT", ' +
+                                  '     "from": "192.0.2.1"},' +
+                                  '    {"action": "REJECT",' +
+                                  '     "from": "192.0.2.0/24"},' +
+                                  '    {"action": "DROP",' +
+                                  '     "from": "2001:db8::1"},' +
+                                  '] }')
         self.assertEqual(ACCEPT, acl.execute(CONTEXT4))
         self.assertEqual(REJECT, acl.execute(get_context('192.0.2.2')))
         self.assertEqual(DROP, acl.execute(get_context('2001:db8::1')))
@@ -195,5 +196,12 @@ class RequestACLTest(unittest.TestCase):
         # type mismatch
         self.assertRaises(TypeError, acl.execute, 'bad parameter')
 
+class RequestLoaderTest(unittest.TestCase):
+    # Note: loading ACLs is tested in other test cases.
+
+    def test_construct(self):
+        # at least for now, we don't allow direct construction.
+        self.assertRaises(Error, RequestLoader)
+
 if __name__ == '__main__':
     unittest.main()