Browse Source

[master] Merge branch 'trac1512'

JINMEI Tatuya 13 years ago
parent
commit
86daa84368

+ 2 - 0
configure.ac

@@ -1067,6 +1067,8 @@ AC_CONFIG_FILES([Makefile
                  src/lib/python/isc/testutils/Makefile
                  src/lib/python/isc/bind10/Makefile
                  src/lib/python/isc/bind10/tests/Makefile
+                 src/lib/python/isc/ddns/Makefile
+                 src/lib/python/isc/ddns/tests/Makefile
                  src/lib/python/isc/xfrin/Makefile
                  src/lib/python/isc/xfrin/tests/Makefile
                  src/lib/python/isc/server_common/Makefile

+ 4 - 0
src/lib/dns/message.cc

@@ -561,6 +561,10 @@ Message::removeRRset(const Section section, RRsetIterator& iterator) {
 
 void
 Message::clearSection(const Section section) {
+    if (impl_->mode_ != Message::RENDER) {
+        isc_throw(InvalidMessageOperation,
+                  "clearSection performed in non-render mode");
+    }
     if (section >= MessageImpl::NUM_SECTIONS) {
         isc_throw(OutOfRange, "Invalid message section: " << section);
     }

+ 6 - 0
src/lib/dns/message.h

@@ -513,6 +513,12 @@ public:
 
     /// \brief Remove all RRSets from the given Section
     ///
+    /// This method is only allowed in the \c RENDER mode, and the given
+    /// section must be valid.
+    ///
+    /// \throw InvalidMessageOperation Message is not in the \c RENDER mode
+    /// \throw OutOfRange The specified section is not valid
+    ///
     /// \param section Section to remove all rrsets from
     void clearSection(const Section section);
 

+ 27 - 0
src/lib/dns/python/message_python.cc

@@ -76,6 +76,7 @@ PyObject* Message_getSection(PyObject* self, PyObject* args);
 PyObject* Message_addQuestion(s_Message* self, PyObject* args);
 PyObject* Message_addRRset(s_Message* self, PyObject* args);
 PyObject* Message_clear(s_Message* self, PyObject* args);
+PyObject* Message_clearSection(PyObject* pyself, PyObject* args);
 PyObject* Message_makeResponse(s_Message* self);
 PyObject* Message_toText(s_Message* self);
 PyObject* Message_str(PyObject* self);
@@ -149,6 +150,8 @@ PyMethodDef Message_methods[] = {
       "Clears the message content (if any) and reinitialize the "
       "message in the given mode\n"
       "The argument must be either Message.PARSE or Message.RENDER"},
+    { "clear_section", Message_clearSection, METH_VARARGS,
+      Message_clearSection_doc },
     { "make_response", reinterpret_cast<PyCFunction>(Message_makeResponse), METH_NOARGS,
       "Prepare for making a response from a request.\n"
       "This will clear the DNS header except those fields that should be kept "
@@ -564,6 +567,30 @@ Message_clear(s_Message* self, PyObject* args) {
 }
 
 PyObject*
+Message_clearSection(PyObject* pyself, PyObject* args) {
+    s_Message* const self = static_cast<s_Message*>(pyself);
+    int section;
+
+    if (!PyArg_ParseTuple(args, "i", &section)) {
+        return (NULL);
+    }
+    try {
+        self->cppobj->clearSection(static_cast<Message::Section>(section));
+        Py_RETURN_NONE;
+    } catch (const InvalidMessageOperation& imo) {
+        PyErr_SetString(po_InvalidMessageOperation, imo.what());
+        return (NULL);
+    } catch (const isc::OutOfRange& ex) {
+        PyErr_SetString(PyExc_OverflowError, ex.what());
+        return (NULL);
+    } catch (...) {
+        PyErr_SetString(po_IscException,
+                        "Unexpected exception in adding RRset");
+        return (NULL);
+    }
+}
+
+PyObject*
 Message_makeResponse(s_Message* self) {
     self->cppobj->makeResponse();
     Py_RETURN_NONE;

+ 17 - 0
src/lib/dns/python/message_python_inc.cc

@@ -38,4 +38,21 @@ Parameters:\n\
   options    Parse options\n\
 \n\
 ";
+
+const char* const Message_clearSection_doc = "\
+clear_section(section) -> void\n\
+\n\
+Remove all RRSets from the given Section.\n\
+\n\
+This method is only allowed in the RENDER mode, and the given section\n\
+must be valid.\n\
+\n\
+Exceptions:\n\
+  InvalidMessageOperation Message is not in the RENDER mode\n\
+  OverflowError The specified section is not valid\n\
+\n\
+Parameters:\n\
+  section    Section to remove all rrsets from\n\
+\n\
+";
 } // unnamed namespace

+ 9 - 1
src/lib/dns/python/name_python.cc

@@ -20,6 +20,7 @@
 #include <dns/exceptions.h>
 #include <dns/messagerenderer.h>
 #include <dns/name.h>
+#include <dns/labelsequence.h>
 
 #include "pydnspp_common.h"
 #include "messagerenderer_python.h"
@@ -114,6 +115,7 @@ PyObject* Name_reverse(s_Name* self);
 PyObject* Name_concatenate(s_Name* self, PyObject* args);
 PyObject* Name_downcase(s_Name* self);
 PyObject* Name_isWildCard(s_Name* self);
+long Name_hash(PyObject* py_self);
 
 PyMethodDef Name_methods[] = {
     { "at", reinterpret_cast<PyCFunction>(Name_at), METH_VARARGS,
@@ -518,6 +520,12 @@ Name_isWildCard(s_Name* self) {
     }
 }
 
+long
+Name_hash(PyObject* pyself) {
+    s_Name* const self = static_cast<s_Name*>(pyself);
+    return (LabelSequence(*self->cppobj).getHash(false));
+}
+
 } // end of unnamed namespace
 
 namespace isc {
@@ -615,7 +623,7 @@ PyTypeObject name_type = {
     NULL,                               // tp_as_number
     NULL,                               // tp_as_sequence
     NULL,                               // tp_as_mapping
-    NULL,                               // tp_hash
+    Name_hash,                          // tp_hash
     NULL,                               // tp_call
     Name_str,                           // tp_str
     NULL,                               // tp_getattro

+ 8 - 1
src/lib/dns/python/rrclass_python.cc

@@ -52,6 +52,7 @@ PyObject* RRClass_str(PyObject* self);
 PyObject* RRClass_toWire(s_RRClass* self, PyObject* args);
 PyObject* RRClass_getCode(s_RRClass* self);
 PyObject* RRClass_richcmp(s_RRClass* self, s_RRClass* other, int op);
+long RRClass_hash(PyObject* pyself);
 
 // Static function for direct class creation
 PyObject* RRClass_IN(s_RRClass *self);
@@ -264,6 +265,12 @@ PyObject* RRClass_ANY(s_RRClass*) {
     return (RRClass_createStatic(RRClass::ANY()));
 }
 
+long
+RRClass_hash(PyObject* pyself) {
+    s_RRClass* const self = static_cast<s_RRClass*>(pyself);
+    return (self->cppobj->getCode());
+}
+
 } // end anonymous namespace
 
 namespace isc {
@@ -296,7 +303,7 @@ PyTypeObject rrclass_type = {
     NULL,                               // tp_as_number
     NULL,                               // tp_as_sequence
     NULL,                               // tp_as_mapping
-    NULL,                               // tp_hash
+    RRClass_hash,                       // tp_hash
     NULL,                               // tp_call
     RRClass_str,                        // tp_str
     NULL,                               // tp_getattro

+ 20 - 0
src/lib/dns/python/tests/message_python_test.py

@@ -289,6 +289,26 @@ class MessageTest(unittest.TestCase):
         self.assertRaises(TypeError, self.r.clear, "wrong")
         self.assertRaises(TypeError, self.r.clear, 3)
 
+    def test_clear_question_section(self):
+        self.r.add_question(Question(Name("www.example.com"), RRClass.IN(),
+                                     RRType.A()))
+        self.assertEqual(1, self.r.get_rr_count(Message.SECTION_QUESTION))
+        self.r.clear_section(Message.SECTION_QUESTION)
+        self.assertEqual(0, self.r.get_rr_count(Message.SECTION_QUESTION))
+
+    def test_clear_section(self):
+        for section in [Message.SECTION_ANSWER, Message.SECTION_AUTHORITY,
+                        Message.SECTION_ADDITIONAL]:
+            self.r.add_rrset(section, self.rrset_a)
+            self.assertEqual(2, self.r.get_rr_count(section))
+            self.r.clear_section(section)
+            self.assertEqual(0, self.r.get_rr_count(section))
+
+        self.assertRaises(InvalidMessageOperation, self.p.clear_section,
+                          Message.SECTION_ANSWER)
+        self.assertRaises(OverflowError, self.r.clear_section,
+                          self.bogus_section)
+
     def test_to_wire(self):
         self.assertRaises(TypeError, self.r.to_wire, 1)
         self.assertRaises(InvalidMessageOperation,

+ 22 - 0
src/lib/dns/python/tests/name_python_test.py

@@ -218,5 +218,27 @@ class NameTest(unittest.TestCase):
         self.assertTrue(self.name4 <= self.name1)
         self.assertFalse(self.name2 >= self.name1)
 
+    def test_hash(self):
+        # The same name should have the same hash value.
+        self.assertEqual(hash(Name('example.com')), hash(Name('example.com')))
+        # Hash is case insensitive.
+        self.assertEqual(hash(Name('example.com')), hash(Name('EXAMPLE.COM')))
+
+        # These pairs happen to be known to have different hashes.
+        # It may be naive to assume the hash value is always the same (we use
+        # an external library and it depends on its internal details).  If
+        # it turns out that this assumption isn't always held, we should
+        # disable this test.
+        self.assertNotEqual(hash(Name('example.com')),
+                            hash(Name('example.org')))
+
+        # Check insensitiveness for the case of inequality.
+        # Based on the assumption above, this 'if' should be true and
+        # we'll always test the case inside it.  We'll still keep the if in
+        # case we end up disabling the above test.
+        if hash(Name('example.com')) != hash(Name('example.org')):
+            self.assertNotEqual(hash(Name('example.com')),
+                                hash(Name('EXAMPLE.ORG')))
+
 if __name__ == '__main__':
     unittest.main()

+ 8 - 0
src/lib/dns/python/tests/rrclass_python_test.py

@@ -78,6 +78,14 @@ class RRClassTest(unittest.TestCase):
         self.assertTrue(self.c1 <= self.c2)
         self.assertFalse(self.c1 != other_rrclass)
 
+    def test_hash(self):
+        # Exploiting the knowledge that the hash value is the numeric class
+        # value, we can predict the comparison result.
+        self.assertEqual(hash(RRClass.IN()), hash(RRClass("IN")))
+        self.assertEqual(hash(RRClass("in")), hash(RRClass("IN")))
+        self.assertNotEqual(hash(RRClass.IN()), hash(RRClass.CH()))
+        self.assertNotEqual(hash(RRClass.IN()), hash(RRClass("CLASS65535")))
+
     def test_statics(self):
         self.assertEqual(RRClass.IN(), RRClass("IN"))
         self.assertEqual(RRClass.CH(), RRClass("CH"))

+ 7 - 0
src/lib/dns/tests/message_unittest.cc

@@ -466,6 +466,13 @@ TEST_F(MessageTest, clearAdditionalSection) {
     EXPECT_EQ(0, message_render.getRRCount(Message::SECTION_ADDITIONAL));
 }
 
+TEST_F(MessageTest, badClearSection) {
+    // attempt of clearing a message in the parse mode.
+    EXPECT_THROW(message_parse.clearSection(Message::SECTION_QUESTION),
+                 InvalidMessageOperation);
+    // attempt of clearing out-of-range section
+    EXPECT_THROW(message_render.clearSection(bogus_section), OutOfRange);
+}
 
 TEST_F(MessageTest, badBeginSection) {
     // valid cases are tested via other tests

+ 1 - 1
src/lib/python/isc/Makefile.am

@@ -1,5 +1,5 @@
 SUBDIRS = datasrc cc config dns log net notify util testutils acl bind10
-SUBDIRS += xfrin log_messages server_common
+SUBDIRS += xfrin log_messages server_common ddns
 
 python_PYTHON = __init__.py
 

+ 23 - 0
src/lib/python/isc/ddns/Makefile.am

@@ -0,0 +1,23 @@
+SUBDIRS = . tests
+
+python_PYTHON = __init__.py session.py logger.py zone_config.py
+BUILT_SOURCES = $(PYTHON_LOGMSGPKG_DIR)/work/libddns_messages.py
+nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/libddns_messages.py
+pylogmessagedir = $(pyexecdir)/isc/log_messages/
+
+EXTRA_DIST = libddns_messages.mes
+
+CLEANFILES = $(PYTHON_LOGMSGPKG_DIR)/work/libddns_messages.py
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/libddns_messages.pyc
+
+# Define rule to build logging source files from message file
+$(PYTHON_LOGMSGPKG_DIR)/work/libddns_messages.py: libddns_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message \
+		-d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/libddns_messages.mes
+
+pythondir = $(pyexecdir)/isc/ddns
+
+CLEANDIRS = __pycache__
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 0 - 0
src/lib/python/isc/ddns/__init__.py


+ 41 - 0
src/lib/python/isc/ddns/libddns_messages.mes

@@ -0,0 +1,41 @@
+# Copyright (C) 2012  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.
+
+# No namespace declaration - these constants go in the global namespace
+# of the libddns_messages python module.
+
+% LIBDDNS_UPDATE_ERROR update client %1 for zone %2: %3
+Debug message.  An error is found in processing a dynamic update
+request.  This log message is used for general errors that are not
+normally expected to happen.  So, in general, it would mean some
+problem in the client implementation or an interoperability issue
+with this implementation.  The client's address, the zone name and
+class, and description of the error are logged.
+
+% LIBDDNS_UPDATE_FORWARD_FAIL update client %1 for zone %2: update forwarding not supported
+Debug message.  An update request is sent to a secondary server.  This
+is not necessarily invalid, but this implementation does not yet
+support update forwarding as specified in Section 6 of RFC2136 and it
+will simply return a response with an RCODE of NOTIMP to the client.
+The client's address and the zone name/class are logged.
+
+% LIBDDNS_UPDATE_NOTAUTH update client %1 for zone %2: not authoritative for update zone
+Debug message.  An update request for a zone for which the receiving
+server doesn't have authority.  In theory this is an unexpected event,
+but there are client implementations that could send update requests
+carelessly, so it may not necessarily be so uncommon in practice.  If
+possible, you may want to check the implementation or configuration of
+those clients to suppress the requests.  As specified in Section 3.1
+of RFC2136, the receiving server will return a response with an RCODE
+of NOTAUTH.

+ 87 - 0
src/lib/python/isc/ddns/logger.py

@@ -0,0 +1,87 @@
+# Copyright (C) 2012  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.
+
+""" This is a logging utility module for other modules of the ddns library
+package.
+
+"""
+
+import isc.log
+
+# The logger for this package
+logger = isc.log.Logger('libddns')
+
+class ClientFormatter:
+    """A utility class to convert a client address to string.
+
+    This class is constructed with a Python standard socket address tuple.
+    If it's 2-element tuple, it's assumed to be an IPv4 socket address
+    and will be converted to the form of '<addr>:<port>'.
+    If it's 4-element tuple, it's assumed to be an IPv6 socket address.
+    and will be converted to the form of '[<addr>]:<por>'.
+
+    This class is designed to delay the conversion until it's explicitly
+    requested, so the conversion doesn't happen if the corresponding log
+    message is suppressed because of its log level (which is often the case
+    for debug messages).
+
+    Note: this optimization comes with the cost of instantiating the
+    formatter object itself.  It's not really clear which overhead is
+    heavier, and we may conclude it's actually better to just generate
+    the strings unconditionally.  Alternatively, we can make the stored
+    address of this object replaceable so that this object can be reused.
+    Right now this is an open issue.
+
+    """
+    def __init__(self, addr):
+        self.__addr = addr
+
+    def __str__(self):
+        if len(self.__addr) == 2:
+            return self.__addr[0] + ':' + str(self.__addr[1])
+        elif len(self.__addr) == 4:
+            return '[' + self.__addr[0] + ']:' + str(self.__addr[1])
+        return None
+
+class ZoneFormatter:
+    """A utility class to convert zone name and class to string.
+
+    This class is constructed with a name of a zone (isc.dns.Name object)
+    and its RR class (isc.dns.RRClass object).  Its text conversion method
+    (__str__) converts them into a string in the form of
+    '<zone name>/<zone class>' where the trailing dot of the zone name
+    is omitted.
+
+    If the given zone name on construction is None, it's assumed to be
+    the zone isn't identified but needs to be somehow logged.  The conversion
+    method returns a special string to indicate this case.
+
+    This class is designed to delay the conversion until it's explicitly
+    requested, so the conversion doesn't happen if the corresponding log
+    message is suppressed because of its log level (which is often the case
+    for debug messages).
+
+    See the note for the ClientFormatter class about overhead tradeoff.
+    This class shares the same discussion.
+
+    """
+    def __init__(self, zname, zclass):
+        self.__zname = zname
+        self.__zclass = zclass
+
+    def __str__(self):
+        if self.__zname is None:
+            return '(zone unknown/not determined)'
+        return self.__zname.to_text(True) + '/' + self.__zclass.to_text()

+ 189 - 0
src/lib/python/isc/ddns/session.py

@@ -0,0 +1,189 @@
+# Copyright (C) 2012  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.
+
+from isc.dns import *
+import isc.ddns.zone_config
+from isc.log import *
+from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter
+from isc.log_messages.libddns_messages import *
+
+# Result codes for UpdateSession.handle()
+UPDATE_SUCCESS = 0
+UPDATE_ERROR = 1
+UPDATE_DROP = 2
+
+# Convenient aliases of update-specific section names
+SECTION_ZONE = Message.SECTION_QUESTION
+SECTION_PREREQUISITE = Message.SECTION_ANSWER
+SECTION_UPDATE = Message.SECTION_AUTHORITY
+
+# Shortcut
+DBGLVL_TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
+
+class UpdateError(Exception):
+    '''Exception for general error in update request handling.
+
+    This exception is intended to be used internally within this module.
+    When UpdateSession.handle() encounters an error in handling an update
+    request it can raise this exception to terminate the handling.
+
+    This class is constructed with some information that may be useful for
+    subsequent possible logging:
+    - msg (string) A string explaining the error.
+    - zname (isc.dns.Name) The zone name.  Can be None when not identified.
+    - zclass (isc.dns.RRClass) The zone class.  Like zname, can be None.
+    - rcode (isc.dns.RCode) The RCODE to be set in the response message.
+    - nolog (bool) If True, it indicates there's no more need for logging.
+
+    '''
+    def __init__(self, msg, zname, zclass, rcode, nolog=False):
+        Exception.__init__(self, msg)
+        self.zname = zname
+        self.zclass = zclass
+        self.rcode = rcode
+        self.nolog = nolog
+
+class UpdateSession:
+    '''Protocol handling for a single dynamic update request.
+
+    This class is instantiated with a request message and some other
+    information that will be used for handling the request.  Its main
+    method, handle(), will process the request, and normally build
+    a response message according to the result.  The application of this
+    class can use the message to send a response to the client.
+
+    '''
+    def __init__(self, req_message, req_data, client_addr, zone_config):
+        '''Constructor.
+
+        Note: req_data is not really used as of #1512 but is listed since
+        it's quite likely we need it in a subsequent task soon.  We'll
+        also need to get other parameters such as ACLs, for which, it's less
+        clear in which form we want to get the information, so it's left
+        open for now.
+
+        Parameters:
+        - req_message (isc.dns.Message) The request message.  This must be
+          in the PARSE mode.
+        - req_data (binary) Wire format data of the request message.
+          It will be used for TSIG verification if necessary.
+        - client_addr (socket address) The address/port of the update client
+          in the form of Python socket address object.  This is mainly for
+          logging and access control.
+        - zone_config (ZoneConfig) A tentative container that encapsulates
+          the server's zone configuration.  See zone_config.py.
+
+        (It'll soon need to be passed ACL in some way, too)
+
+        '''
+        self.__message = req_message
+        self.__client_addr = client_addr
+        self.__zone_config = zone_config
+
+    def get_message(self):
+        '''Return the update message.
+
+        After handle() is called, it's generally transformed to the response
+        to be returned to the client; otherwise it would be identical to
+        the request message passed on construction.
+
+        '''
+        return self.__message
+
+    def handle(self):
+        '''Handle the update request according to RFC2136.
+
+        This method returns a tuple of the following three elements that
+        indicate the result of the request.
+        - Result code of the request processing, which are:
+          UPDATE_SUCCESS Update request granted and succeeded.
+          UPDATE_ERROR Some error happened to be reported in the response.
+          UPDATE_DROP Error happened and no response should be sent.
+          Except the case of UPDATE_DROP, the UpdateSession object will have
+          created a response that is to be returned to the request client,
+          which can be retrieved by get_message().
+        - The name of the updated zone (isc.dns.Name object) in case of
+          UPDATE_SUCCESS; otherwise None.
+        - The RR class of the updated zone (isc.dns.RRClass object) in case
+          of UPDATE_SUCCESS; otherwise None.
+
+        '''
+        try:
+            datasrc_client, zname, zclass = self.__get_update_zone()
+            # conceptual code that would follow
+            # self.__check_prerequisites()
+            # self.__check_update_acl()
+            # self.__do_update()
+            # self.__make_response(Rcode.NOERROR())
+            return UPDATE_SUCCESS, zname, zclass
+        except UpdateError as e:
+            if not e.nolog:
+                logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_ERROR,
+                             ClientFormatter(self.__client_addr),
+                             ZoneFormatter(e.zname, e.zclass), e)
+            self.__make_response(e.rcode)
+            return UPDATE_ERROR, None, None
+
+    def __get_update_zone(self):
+        '''Parse the zone section and find the zone to be updated.
+
+        If the zone section is valid and the specified zone is found in
+        the configuration, it returns a tuple of:
+        - A matching data source that contains the specified zone
+        - The zone name as a Name object
+        - The zone class as an RRClass object
+
+        '''
+        # Validation: the zone section must contain exactly one question,
+        # and it must be of type SOA.
+        n_zones = self.__message.get_rr_count(SECTION_ZONE)
+        if n_zones != 1:
+            raise UpdateError('Invalid number of records in zone section: ' +
+                              str(n_zones), None, None, Rcode.FORMERR())
+        zrecord = self.__message.get_question()[0]
+        if zrecord.get_type() != RRType.SOA():
+            raise UpdateError('update zone section contains non-SOA',
+                              None, None, Rcode.FORMERR())
+
+        # See if we're serving a primary zone specified in the zone section.
+        zname = zrecord.get_name()
+        zclass = zrecord.get_class()
+        zone_type, datasrc_client = self.__zone_config.find_zone(zname, zclass)
+        if zone_type == isc.ddns.zone_config.ZONE_PRIMARY:
+            return datasrc_client, zname, zclass
+        elif zone_type == isc.ddns.zone_config.ZONE_SECONDARY:
+            # We are a secondary server; since we don't yet support update
+            # forwarding, we return 'not implemented'.
+            logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_FORWARD_FAIL,
+                         ClientFormatter(self.__client_addr),
+                         ZoneFormatter(zname, zclass))
+            raise UpdateError('forward', zname, zclass, Rcode.NOTIMP(), True)
+        # zone wasn't found
+        logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_NOTAUTH,
+                     ClientFormatter(self.__client_addr),
+                     ZoneFormatter(zname, zclass))
+        raise UpdateError('notauth', zname, zclass, Rcode.NOTAUTH(), True)
+
+    def __make_response(self, rcode):
+        '''Transform the internal message to the update response.
+
+        According RFC2136 Section 3.8, the zone section will be cleared
+        as well as other sections.  The response Rcode will be set to the
+        given value.
+
+        '''
+        self.__message.make_response()
+        self.__message.clear_section(SECTION_ZONE)
+        self.__message.set_rcode(rcode)

+ 28 - 0
src/lib/python/isc/ddns/tests/Makefile.am

@@ -0,0 +1,28 @@
+PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
+PYTESTS = session_tests.py zone_config_tests.py
+EXTRA_DIST = $(PYTESTS)
+CLEANFILES = $(builddir)/rwtest.sqlite3.copied
+
+# If necessary (rare cases), explicitly specify paths to dynamic libraries
+# required by loadable python modules.
+if SET_ENV_LIBRARY_PATH
+LIBRARY_PATH_PLACEHOLDER = $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
+endif
+
+# test using command-line arguments, so use check-local target instead of TESTS
+# B10_FROM_BUILD is necessary to load data source backend from the build tree.
+check-local:
+if ENABLE_PYTHON_COVERAGE
+	touch $(abs_top_srcdir)/.coverage
+	rm -f .coverage
+	${LN_S} $(abs_top_srcdir)/.coverage .coverage
+endif
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	$(LIBRARY_PATH_PLACEHOLDER) \
+	TESTDATA_PATH=$(abs_top_srcdir)/src/lib/testutils/testdata \
+	TESTDATA_WRITE_PATH=$(builddir) \
+	B10_FROM_BUILD=$(abs_top_builddir) \
+	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/dns/python/.libs \
+	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+	done

+ 154 - 0
src/lib/python/isc/ddns/tests/session_tests.py

@@ -0,0 +1,154 @@
+# Copyright (C) 2012  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 os
+import shutil
+import isc.log
+import unittest
+from isc.dns import *
+from isc.datasrc import DataSourceClient
+from isc.ddns.session import *
+from isc.ddns.zone_config import *
+
+# Some common test parameters
+TESTDATA_PATH = os.environ['TESTDATA_PATH'] + os.sep
+READ_ZONE_DB_FILE = TESTDATA_PATH + "rwtest.sqlite3" # original, to be copied
+TESTDATA_WRITE_PATH = os.environ['TESTDATA_WRITE_PATH'] + os.sep
+WRITE_ZONE_DB_FILE = TESTDATA_WRITE_PATH + "rwtest.sqlite3.copied"
+WRITE_ZONE_DB_CONFIG = "{ \"database_file\": \"" + WRITE_ZONE_DB_FILE + "\"}"
+
+TEST_ZONE_NAME = Name('example.org')
+UPDATE_RRTYPE = RRType.SOA()
+TEST_RRCLASS = RRClass.IN()
+TEST_ZONE_RECORD = Question(TEST_ZONE_NAME, TEST_RRCLASS, UPDATE_RRTYPE)
+TEST_CLIENT6 = ('2001:db8::1', 53, 0, 0)
+TEST_CLIENT4 = ('192.0.2.1', 53)
+
+def create_update_msg(zones=[TEST_ZONE_RECORD]):
+    msg = Message(Message.RENDER)
+    msg.set_qid(5353)           # arbitrary chosen
+    msg.set_opcode(Opcode.UPDATE())
+    msg.set_rcode(Rcode.NOERROR())
+    for z in zones:
+        msg.add_question(z)
+
+    renderer = MessageRenderer()
+    msg.to_wire(renderer)
+
+    # re-read the created data in the parse mode
+    msg.clear(Message.PARSE)
+    msg.from_wire(renderer.get_data())
+
+    return renderer.get_data(), msg
+
+class SessionTest(unittest.TestCase):
+    '''Session tests'''
+    def setUp(self):
+        shutil.copyfile(READ_ZONE_DB_FILE, WRITE_ZONE_DB_FILE)
+        self.__datasrc_client = DataSourceClient("sqlite3",
+                                                 WRITE_ZONE_DB_CONFIG)
+        self.__update_msgdata, self.__update_msg = create_update_msg()
+        self.__session = UpdateSession(self.__update_msg,
+                                       self.__update_msgdata, TEST_CLIENT4,
+                                       ZoneConfig([], TEST_RRCLASS,
+                                                  self.__datasrc_client))
+
+    def check_response(self, msg, expected_rcode):
+        '''Perform common checks on update resposne message.'''
+        self.assertTrue(msg.get_header_flag(Message.HEADERFLAG_QR))
+        # note: we convert opcode to text it'd be more helpful on failure.
+        self.assertEqual(Opcode.UPDATE().to_text(), msg.get_opcode().to_text())
+        self.assertEqual(expected_rcode.to_text(), msg.get_rcode().to_text())
+        # All sections should be cleared
+        self.assertEqual(0, msg.get_rr_count(SECTION_ZONE))
+        self.assertEqual(0, msg.get_rr_count(SECTION_PREREQUISITE))
+        self.assertEqual(0, msg.get_rr_count(SECTION_UPDATE))
+        self.assertEqual(0, msg.get_rr_count(Message.SECTION_ADDITIONAL))
+
+    def test_handle(self):
+        '''Basic update case'''
+        result, zname, zclass = self.__session.handle()
+        self.assertEqual(UPDATE_SUCCESS, result)
+        self.assertEqual(TEST_ZONE_NAME, zname)
+        self.assertEqual(TEST_RRCLASS, zclass)
+
+        # Just checking these are different from the success code.
+        self.assertNotEqual(UPDATE_ERROR, result)
+        self.assertNotEqual(UPDATE_DROP, result)
+
+    def test_broken_request(self):
+        # Zone section is empty
+        msg_data, msg = create_update_msg(zones=[])
+        session = UpdateSession(msg, msg_data, TEST_CLIENT6, None)
+        result, zname, zclass = session.handle()
+        self.assertEqual(UPDATE_ERROR, result)
+        self.assertEqual(None, zname)
+        self.assertEqual(None, zclass)
+        self.check_response(session.get_message(), Rcode.FORMERR())
+
+        # Zone section contains multiple records
+        msg_data, msg = create_update_msg(zones=[TEST_ZONE_RECORD,
+                                                 TEST_ZONE_RECORD])
+        session = UpdateSession(msg, msg_data, TEST_CLIENT4, None)
+        self.assertEqual(UPDATE_ERROR, session.handle()[0])
+        self.check_response(session.get_message(), Rcode.FORMERR())
+
+        # Zone section's type is not SOA
+        msg_data, msg = create_update_msg(zones=[Question(TEST_ZONE_NAME,
+                                                          TEST_RRCLASS,
+                                                          RRType.A())])
+        session = UpdateSession(msg, msg_data, TEST_CLIENT4, None)
+        self.assertEqual(UPDATE_ERROR, session.handle()[0])
+        self.check_response(session.get_message(), Rcode.FORMERR())
+
+    def test_update_secondary(self):
+        # specified zone is configured as a secondary.  Since this
+        # implementation doesn't support update forwarding, the result
+        # should be NOTIMP.
+        msg_data, msg = create_update_msg(zones=[Question(TEST_ZONE_NAME,
+                                                          TEST_RRCLASS,
+                                                          RRType.SOA())])
+        session = UpdateSession(msg, msg_data, TEST_CLIENT4,
+                                ZoneConfig([(TEST_ZONE_NAME, TEST_RRCLASS)],
+                                           TEST_RRCLASS,
+                                           self.__datasrc_client))
+        self.assertEqual(UPDATE_ERROR, session.handle()[0])
+        self.check_response(session.get_message(), Rcode.NOTIMP())
+
+    def check_notauth(self, zname, zclass=TEST_RRCLASS):
+        '''Common test sequence for the 'notauth' test'''
+        msg_data, msg = create_update_msg(zones=[Question(zname, zclass,
+                                                          RRType.SOA())])
+        session = UpdateSession(msg, msg_data, TEST_CLIENT4,
+                                ZoneConfig([(TEST_ZONE_NAME, TEST_RRCLASS)],
+                                           TEST_RRCLASS,
+                                           self.__datasrc_client))
+        self.assertEqual(UPDATE_ERROR, session.handle()[0])
+        self.check_response(session.get_message(), Rcode.NOTAUTH())
+
+    def test_update_notauth(self):
+        '''Update attempt for non authoritative zones'''
+        # zone name doesn't match
+        self.check_notauth(Name('example.com'))
+        # zone name is a subdomain of the actual authoritative zone
+        # (match must be exact)
+        self.check_notauth(Name('sub.example.org'))
+        # zone class doesn't match
+        self.check_notauth(Name('example.org'), RRClass.CH())
+
+if __name__ == "__main__":
+    isc.log.init("bind10")
+    isc.log.resetUnitTestRootLogger()
+    unittest.main()

+ 117 - 0
src/lib/python/isc/ddns/tests/zone_config_tests.py

@@ -0,0 +1,117 @@
+# Copyright (C) 2012  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 isc.log
+import unittest
+from isc.dns import *
+from isc.datasrc import DataSourceClient
+from isc.ddns.zone_config import *
+
+# Some common test parameters
+TEST_ZONE_NAME = Name('example.org')
+TEST_SECONDARY_ZONE_NAME = Name('example.com')
+TEST_RRCLASS = RRClass.IN()
+
+class FakeDataSourceClient:
+    '''Faked data source client used in the ZoneConfigTest.
+
+    It emulates isc.datasrc.DataSourceClient, but only has to provide
+    the find_zone() interface (and only the first element of the return
+    value matters).  By default it returns 'SUCCESS' (exact match) for
+    any input.  It can be dynamically customized via the set_find_result()
+    method.
+
+    '''
+    def __init__(self):
+        self.__find_result = DataSourceClient.SUCCESS
+
+    def find_zone(self, zname):
+        return (self.__find_result, None)
+
+    def set_find_result(self, result):
+        self.__find_result = result
+
+class ZoneConfigTest(unittest.TestCase):
+    '''Some basic tests for the ZoneConfig class.'''
+    def setUp(self):
+        self.__datasrc_client = FakeDataSourceClient()
+        self.zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS)],
+                                  TEST_RRCLASS, self.__datasrc_client)
+
+    def test_find_zone(self):
+        # Primay zone case: zone is in the data source, and not in secondaries
+        self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
+                         (self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+
+        # Secondary zone case: zone is in the data source and in secondaries.
+        self.assertEqual((ZONE_SECONDARY, None),
+                         (self.zconfig.find_zone(TEST_SECONDARY_ZONE_NAME,
+                                                 TEST_RRCLASS)))
+
+        # 'not found' case: zone not in the data source.
+        self.__datasrc_client.set_find_result(DataSourceClient.NOTFOUND)
+        self.assertEqual((ZONE_NOTFOUND, None),
+                         (self.zconfig.find_zone(Name('example'),
+                                                 TEST_RRCLASS)))
+        # same for the partial match
+        self.__datasrc_client.set_find_result(DataSourceClient.PARTIALMATCH)
+        self.assertEqual((ZONE_NOTFOUND, None),
+                         (self.zconfig.find_zone(Name('example'),
+                                                 TEST_RRCLASS)))
+        # a bit unusual case: zone not in the data source, but in secondaries.
+        # this is probably a configuration error, but ZoneConfig doesn't do
+        # this level check.
+        self.__datasrc_client.set_find_result(DataSourceClient.NOTFOUND)
+        self.assertEqual((ZONE_NOTFOUND, None),
+                         (self.zconfig.find_zone(TEST_ZONE_NAME,
+                                                 TEST_RRCLASS)))
+        # zone class doesn't match (but zone name matches)
+        self.__datasrc_client.set_find_result(DataSourceClient.SUCCESS)
+        zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS)],
+                             RRClass.CH(), self.__datasrc_client)
+        self.assertEqual((ZONE_NOTFOUND, None),
+                         (zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+        # similar to the previous case, but also in the secondary list
+        zconfig = ZoneConfig([(TEST_ZONE_NAME, TEST_RRCLASS)],
+                             RRClass.CH(), self.__datasrc_client)
+        self.assertEqual((ZONE_NOTFOUND, None),
+                         (zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+
+        # check some basic tests varying the secondary list.
+        # empty secondary list doesn't cause any disruption.
+        zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
+        self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
+                         (self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+        # adding some mulitle tuples, including subdomainof the test zone name,
+        # and the same zone name but a different class
+        zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS),
+                              (Name('example'), TEST_RRCLASS),
+                              (Name('sub.example.org'), TEST_RRCLASS),
+                              (TEST_ZONE_NAME, RRClass.CH())],
+                             TEST_RRCLASS, self.__datasrc_client)
+        self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
+                         (self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+        # secondary zone list has a duplicate entry, which is just
+        # (effecitivey) ignored
+        zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS),
+                              (TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS)],
+                             TEST_RRCLASS, self.__datasrc_client)
+        self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
+                         (self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+
+if __name__ == "__main__":
+    isc.log.init("bind10")
+    isc.log.resetUnitTestRootLogger()
+    unittest.main()

+ 64 - 0
src/lib/python/isc/ddns/zone_config.py

@@ -0,0 +1,64 @@
+# Copyright (C) 2012  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 isc.dns
+from isc.datasrc import DataSourceClient
+
+# Constants representing zone types
+ZONE_NOTFOUND = -1              # Zone isn't found in find_zone()
+ZONE_PRIMARY = 0                # Primary zone
+ZONE_SECONDARY = 1              # Secondary zone
+
+class ZoneConfig:
+    '''A temporary helper class to encapsulate zone related configuration.
+
+    Its find_zone method will search the conceptual configuration for a
+    given zone, and return a tuple of zone type (primary or secondary) and
+    the client object to access the data source stroing the zone.
+    It's very likely that details of zone related configurations like this
+    will change in near future, so the main purpose of this class is to
+    provide an independent interface for the main DDNS session module
+    until the details are fixed.
+
+    '''
+    def __init__(self, secondaries, datasrc_class, datasrc_client):
+        '''Constructor.
+
+        Parameters:
+        - secondaries: a list of 2-element tuples.  Each element is a pair
+          of isc.dns.Name and isc.dns.RRClass, and identifies a single
+          secondary zone.
+        - datasrc_class: isc.dns.RRClass object.  Specifies the RR class
+          of datasrc_client.
+        - datasrc_client: isc.dns.DataSourceClient object.  A data source
+          class for the RR class of datasrc_class.  It's expected to contain
+          a zone that is eventually updated in the ddns package.
+
+        '''
+        self.__secondaries = set()
+        for (zname, zclass) in secondaries:
+            self.__secondaries.add((zname, zclass))
+        self.__datasrc_class = datasrc_class
+        self.__datasrc_client = datasrc_client
+
+    def find_zone(self, zone_name, zone_class):
+        '''Return the type and accessor client object for given zone.'''
+        if self.__datasrc_class == zone_class and \
+                self.__datasrc_client.find_zone(zone_name)[0] == \
+                DataSourceClient.SUCCESS:
+            if (zone_name, zone_class) in self.__secondaries:
+                return ZONE_SECONDARY, None
+            return ZONE_PRIMARY, self.__datasrc_client
+        return ZONE_NOTFOUND, None

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

@@ -12,6 +12,7 @@ EXTRA_DIST += zonemgr_messages.py
 EXTRA_DIST += cfgmgr_messages.py
 EXTRA_DIST += config_messages.py
 EXTRA_DIST += notify_out_messages.py
+EXTRA_DIST += libddns_messages.py
 EXTRA_DIST += libxfrin_messages.py
 EXTRA_DIST += server_common_messages.py
 EXTRA_DIST += dbutil_messages.py
@@ -28,6 +29,7 @@ CLEANFILES += zonemgr_messages.pyc
 CLEANFILES += cfgmgr_messages.pyc
 CLEANFILES += config_messages.pyc
 CLEANFILES += notify_out_messages.pyc
+CLEANFILES += libddns_messages.pyc
 CLEANFILES += libxfrin_messages.pyc
 CLEANFILES += server_common_messages.pyc
 CLEANFILES += dbutil_messages.pyc

+ 1 - 0
src/lib/python/isc/log_messages/libddns_messages.py

@@ -0,0 +1 @@
+from work.libddns_messages import *