Browse Source

[master] Merge branch 'trac2380merge2'

Jelte Jansen 12 years ago
parent
commit
689b015753
49 changed files with 1070 additions and 1086 deletions
  1. 2 4
      configure.ac
  2. 3 10
      doc/guide/bind10-guide.xml
  3. 1 1
      src/bin/loadzone/.gitignore
  4. 19 10
      src/bin/loadzone/Makefile.am
  5. 0 13
      src/bin/loadzone/TODO
  6. 0 94
      src/bin/loadzone/b10-loadzone.py.in
  7. 124 18
      src/bin/loadzone/b10-loadzone.xml
  8. 342 0
      src/bin/loadzone/loadzone.py.in
  9. 81 0
      src/bin/loadzone/loadzone_messages.mes
  10. 37 0
      src/bin/loadzone/tests/Makefile.am
  11. 4 1
      src/bin/loadzone/tests/correct/Makefile.am
  12. 9 9
      src/bin/loadzone/tests/correct/correct_test.sh.in
  13. 10 4
      src/bin/loadzone/tests/correct/example.db
  14. 6 2
      src/bin/loadzone/tests/correct/include.db
  15. 6 2
      src/bin/loadzone/tests/correct/mix1.db
  16. 6 2
      src/bin/loadzone/tests/correct/mix2.db
  17. 2 2
      src/bin/loadzone/tests/correct/mix2sub2.txt
  18. 6 2
      src/bin/loadzone/tests/correct/ttl1.db
  19. 6 2
      src/bin/loadzone/tests/correct/ttl2.db
  20. 6 2
      src/bin/loadzone/tests/correct/ttlext.db
  21. 0 1
      src/bin/loadzone/tests/error/.gitignore
  22. 0 28
      src/bin/loadzone/tests/error/Makefile.am
  23. 0 11
      src/bin/loadzone/tests/error/error.known
  24. 0 82
      src/bin/loadzone/tests/error/error_test.sh.in
  25. 0 13
      src/bin/loadzone/tests/error/formerr1.db
  26. 0 12
      src/bin/loadzone/tests/error/formerr2.db
  27. 0 12
      src/bin/loadzone/tests/error/formerr3.db
  28. 0 12
      src/bin/loadzone/tests/error/formerr4.db
  29. 0 13
      src/bin/loadzone/tests/error/formerr5.db
  30. 0 1
      src/bin/loadzone/tests/error/include.txt
  31. 0 12
      src/bin/loadzone/tests/error/keyerror1.db
  32. 0 12
      src/bin/loadzone/tests/error/keyerror2.db
  33. 0 13
      src/bin/loadzone/tests/error/keyerror3.db
  34. 0 11
      src/bin/loadzone/tests/error/originerr1.db
  35. 0 12
      src/bin/loadzone/tests/error/originerr2.db
  36. 342 0
      src/bin/loadzone/tests/loadzone_test.py
  37. 11 0
      src/bin/loadzone/tests/testdata/broken-example.org.zone
  38. 10 0
      src/bin/loadzone/tests/testdata/example-nons.org.zone
  39. 3 0
      src/bin/loadzone/tests/testdata/example-nosoa.org.zone
  40. 10 0
      src/bin/loadzone/tests/testdata/example.org.zone
  41. 13 4
      src/lib/dns/master_loader.cc
  42. 4 3
      src/lib/dns/tests/master_loader_unittest.cc
  43. 1 1
      src/lib/python/isc/datasrc/Makefile.am
  44. 0 616
      src/lib/python/isc/datasrc/master.py
  45. 1 2
      src/lib/python/isc/datasrc/tests/Makefile.am
  46. 0 35
      src/lib/python/isc/datasrc/tests/master_test.py
  47. 2 0
      src/lib/python/isc/log_messages/Makefile.am
  48. 1 0
      src/lib/python/isc/log_messages/loadzone_messages.py
  49. 2 2
      tests/system/bindctl/setup.sh

+ 2 - 4
configure.ac

@@ -1176,8 +1176,8 @@ AC_CONFIG_FILES([Makefile
                  src/bin/dbutil/tests/Makefile
                  src/bin/dbutil/tests/testdata/Makefile
                  src/bin/loadzone/Makefile
+                 src/bin/loadzone/tests/Makefile
                  src/bin/loadzone/tests/correct/Makefile
-                 src/bin/loadzone/tests/error/Makefile
                  src/bin/msgq/Makefile
                  src/bin/msgq/tests/Makefile
                  src/bin/auth/Makefile
@@ -1350,8 +1350,7 @@ AC_OUTPUT([doc/version.ent
            src/bin/bindctl/tests/bindctl_test
            src/bin/loadzone/run_loadzone.sh
            src/bin/loadzone/tests/correct/correct_test.sh
-           src/bin/loadzone/tests/error/error_test.sh
-           src/bin/loadzone/b10-loadzone.py
+           src/bin/loadzone/loadzone.py
            src/bin/usermgr/run_b10-cmdctl-usermgr.sh
            src/bin/usermgr/b10-cmdctl-usermgr.py
            src/bin/msgq/msgq.py
@@ -1418,7 +1417,6 @@ AC_OUTPUT([doc/version.ent
            chmod +x src/bin/bindctl/run_bindctl.sh
            chmod +x src/bin/loadzone/run_loadzone.sh
            chmod +x src/bin/loadzone/tests/correct/correct_test.sh
-           chmod +x src/bin/loadzone/tests/error/error_test.sh
            chmod +x src/bin/sysinfo/run_sysinfo.sh
            chmod +x src/bin/usermgr/run_b10-cmdctl-usermgr.sh
            chmod +x src/bin/msgq/run_msgq.sh

+ 3 - 10
doc/guide/bind10-guide.xml

@@ -449,8 +449,10 @@ var/
 
         <listitem>
           <para>Load desired zone file(s), for example:
-            <screen>$ <userinput>b10-loadzone <replaceable>your.zone.example.org</replaceable></userinput></screen>
+            <screen>$ <userinput>b10-loadzone <replaceable>-c '{"database_file": "/usr/local/var/bind10/zone.sqlite3"}'</replaceable> <replaceable>your.zone.example.org</replaceable> <replaceable>your.zone.file</replaceable></userinput></screen>
           </para>
+	  (If you use the sqlite3 data source with the default DB
+	  file, you can omit the -c option).
         </listitem>
 
         <listitem>
@@ -2636,19 +2638,10 @@ can use various data source backends.
 
       </para>
 
-      <para>
-        The <option>-o</option> argument may be used to define the
-        default origin for loaded zone file records.
-      </para>
-
       <note>
       <para>
         In the current release, only the SQLite3 back
         end is used by <command>b10-loadzone</command>.
-        By default, it stores the zone data in
-        <filename>/usr/local/var/bind10-devel/zone.sqlite3</filename>
-        unless the <option>-d</option> switch is used to set the
-        database filename.
         Multiple zones are stored in a single SQLite3 zone database.
       </para>
       </note>

+ 1 - 1
src/bin/loadzone/.gitignore

@@ -1,4 +1,4 @@
 /b10-loadzone
-/b10-loadzone.py
+/loadzone.py
 /run_loadzone.sh
 /b10-loadzone.8

+ 19 - 10
src/bin/loadzone/Makefile.am

@@ -1,12 +1,17 @@
-SUBDIRS = . tests/correct tests/error
+SUBDIRS = . tests
 bin_SCRIPTS = b10-loadzone
 noinst_SCRIPTS = run_loadzone.sh
 
-CLEANFILES = b10-loadzone
+nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/loadzone_messages.py
+pylogmessagedir = $(pyexecdir)/isc/log_messages/
+
+CLEANFILES = b10-loadzone loadzone.pyc
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/loadzone_messages.py
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/loadzone_messages.pyc
 
 man_MANS = b10-loadzone.8
 DISTCLEANFILES = $(man_MANS)
-EXTRA_DIST = $(man_MANS) b10-loadzone.xml
+EXTRA_DIST = $(man_MANS) b10-loadzone.xml loadzone_messages.mes
 
 if GENERATE_DOCS
 
@@ -21,10 +26,13 @@ $(man_MANS):
 
 endif
 
-b10-loadzone: b10-loadzone.py
-	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
-	       -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" \
-	       -e "s|@@LIBEXECDIR@@|$(pkglibexecdir)|" b10-loadzone.py >$@
+# Define rule to build logging source files from message file
+$(PYTHON_LOGMSGPKG_DIR)/work/loadzone_messages.py : loadzone_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message \
+	-d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/loadzone_messages.mes
+
+b10-loadzone: loadzone.py $(PYTHON_LOGMSGPKG_DIR)/work/loadzone_messages.py
+	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" loadzone.py >$@
 	chmod a+x $@
 
 EXTRA_DIST += tests/normal/README
@@ -48,6 +56,7 @@ EXTRA_DIST += tests/normal/sql1.example.com.signed
 EXTRA_DIST += tests/normal/sql2.example.com
 EXTRA_DIST += tests/normal/sql2.example.com.signed
 
-pytest:
-	$(SHELL) tests/correct/correct_test.sh
-	$(SHELL) tests/error/error_test.sh
+CLEANDIRS = __pycache__
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 0 - 13
src/bin/loadzone/TODO

@@ -1,16 +1,3 @@
-Support optional origin in $INCLUDE:
-$INCLUDE filename origin
-
-Support optional comment in $INCLUDE:
-$INCLUDE filename origin comment
-
-Support optional comment in $TTL (RFC 2308):
-$TTL number comment
-
-Do not assume "." is origin if origin is not set and sees a @ or
-a label without a ".". It should probably fail.  (Don't assume a
-mistake means it is a root level label.)
-
 Add verbose option to show what it is adding, not necessarily
 in master file format, but in the context of the data source.
 

+ 0 - 94
src/bin/loadzone/b10-loadzone.py.in

@@ -1,94 +0,0 @@
-#!@PYTHON@
-
-# Copyright (C) 2010  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 sys; sys.path.append ('@@PYTHONPATH@@')
-import re, getopt
-import isc.datasrc
-import isc.util.process
-from isc.datasrc.master import MasterFile
-import time
-import os
-
-isc.util.process.rename()
-
-#########################################################################
-# usage: print usage note and exit
-#########################################################################
-def usage():
-    print("Usage: %s [-d <database>] [-o <origin>] <file>" % sys.argv[0], \
-          file=sys.stderr)
-    exit(1)
-
-#########################################################################
-# main
-#########################################################################
-def main():
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], "d:o:h", \
-                                                ["dbfile", "origin", "help"])
-    except getopt.GetoptError as e:
-        print(str(e))
-        usage()
-        exit(2)
-
-    dbfile = '@@LOCALSTATEDIR@@/@PACKAGE@/zone.sqlite3'
-    initial_origin = ''
-    for o, a in opts:
-        if o in ("-d", "--dbfile"):
-            dbfile = a
-        elif o in ("-o", "--origin"):
-            if a[-1] != '.':
-                a += '.'
-            initial_origin = a
-        elif o in ("-h", "--help"):
-            usage()
-        else:
-            assert False, "unhandled option"
-
-    if len(args) != 1:
-        usage()
-    zonefile = args[0]
-    verbose = os.isatty(sys.stdout.fileno())
-    try:
-        master = MasterFile(zonefile, initial_origin, verbose)
-    except Exception as e:
-        sys.stderr.write("Error reading zone file: %s\n" % str(e))
-        exit(1)
-
-    try:
-        zone = master.zonename()
-        if verbose:
-            sys.stdout.write("Using SQLite3 database file %s\n" % dbfile)
-            sys.stdout.write("Zone name is %s\n" % zone)
-            sys.stdout.write("Loading file \"%s\"\n" % zonefile)
-    except Exception as e:
-        sys.stdout.write("\n")
-        sys.stderr.write("Error reading zone file: %s\n" % str(e))
-        exit(1)
-
-    try:
-        isc.datasrc.sqlite3_ds.load(dbfile, zone, master.zonedata)
-        if verbose:
-            master.closeverbose()
-            sys.stdout.write("\nDone.\n")
-    except Exception as e:
-        sys.stdout.write("\n")
-        sys.stderr.write("Error loading database: %s\n"% str(e))
-        exit(1)
-
-if __name__ == "__main__":
-    main()

+ 124 - 18
src/bin/loadzone/b10-loadzone.xml

@@ -2,7 +2,7 @@
                "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
 	       [<!ENTITY mdash "&#8212;">]>
 <!--
- - Copyright (C) 2010  Internet Systems Consortium, Inc. ("ISC")
+ - 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
@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>March 26, 2012</date>
+    <date>December 15, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -36,7 +36,7 @@
 
   <docinfo>
     <copyright>
-      <year>2010</year>
+      <year>2012</year>
       <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
     </copyright>
   </docinfo>
@@ -44,9 +44,13 @@
   <refsynopsisdiv>
     <cmdsynopsis>
       <command>b10-loadzone</command>
-      <arg><option>-d <replaceable class="parameter">database</replaceable></option></arg>
-      <arg><option>-o <replaceable class="parameter">origin</replaceable></option></arg>
-      <arg choice="req">filename</arg>
+      <arg><option>-c <replaceable class="parameter">datasrc_config</replaceable></option></arg>
+      <arg><option>-d <replaceable class="parameter">debug_level</replaceable></option></arg>
+      <arg><option>-i <replaceable class="parameter">report_interval</replaceable></option></arg>
+      <arg><option>-t <replaceable class="parameter">datasrc_type</replaceable></option></arg>
+      <arg><option>-C <replaceable class="parameter">zone_class</replaceable></option></arg>
+      <arg choice="req">zone name</arg>
+      <arg choice="req">zone file</arg>
     </cmdsynopsis>
   </refsynopsisdiv>
 
@@ -66,8 +70,6 @@
     $ORIGIN is followed by a domain name, and sets the the origin
     that will be used for relative domain names in subsequent records.
     $INCLUDE is followed by a filename to load.
-<!-- TODO: and optionally a
-    domain name used to set the relative domain name origin. -->
     The previous origin is restored after the file is included.
 <!-- the current domain name is also restored -->
     $TTL is followed by a time-to-live value which is used
@@ -75,11 +77,31 @@
     </para>
 
     <para>
+      If the specified zone does not exist in the specified data
+      source, <command>b10-loadzone</command> will first create a
+      new empty zone in the data source, then fill it with the RRs
+      given in the specified master zone file.  In this case, if
+      loading fails for some reason, the creation of the new zone
+      is also canceled.
+      <note><simpara>
+	Due to an implementation limitation, the current version
+	does not make the zone creation and subsequent loading an
+	atomic operation; an empty zone will be visible and used by
+	other application (e.g., the <command>b10-auth</command>
+	authoritative server) while loading.  If this is an issue,
+	make sure the initial loading of a new zone is done before
+	starting other BIND 10 applications.
+      </simpara></note>
+    </para>
+
+    <para>
       When re-loading an existing zone, the prior version is completely
       removed.  While the new version of the zone is being loaded, the old
       version remains accessible to queries.  After the new version is
       completely loaded, the old version is swapped out and replaced
-      with the new one in a single operation.
+      with the new one in a single operation.  If loading fails for
+      some reason, the loaded RRs will be effectively deleted, and the
+      old version will still remain accessible for other applications.
     </para>
 
   </refsect1>
@@ -88,21 +110,82 @@
     <title>ARGUMENTS</title>
 
     <variablelist>
+      <varlistentry>
+        <term>-c <replaceable class="parameter">datasrc_config</replaceable></term>
+        <listitem><para>
+          Specifies configuration of the data source in the JSON
+          format.  The configuration contents depend on the type of
+	  the data source, and that's the same as what would be
+	  specified for the BIND 10 servers (see the data source
+          configuration section of the BIND 10 guide).  For example,
+	  for an SQLite3 data source, it would look like
+	  '{"database_file": "path-to-sqlite3-db-file"}'.
+	  <note>
+	    <simpara>For SQLite3 data source with the default DB file,
+	      this option can be omitted; in other cases including
+	      for any other types of data sources when supported,
+	      this option is currently mandatory in practice.
+	      In a future version it will be possible to retrieve the
+	      configuration from the BIND 10 server configuration (if
+	      it exists).
+	  </simpara></note>
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-d <replaceable class="parameter">debug_level</replaceable> </term>
+        <listitem><para>
+	    Enable dumping debug level logging with the specified
+	    level.  By default, only log messages at the severity of
+	    informational or higher levels will be produced.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-i <replaceable class="parameter">report_interval</replaceable></term>
+        <listitem><para>
+          Specifies the interval of status update by the number of RRs
+	  loaded in the interval.
+	  The <command>b10-loadzone</command> tool periodically
+          reports the progress of loading with the total number of
+          loaded RRs and elapsed time.  This option specifies the
+	  interval of the reports.  If set to 0, status reports will
+          be suppressed.  The default is 10,000.
+        </para></listitem>
+      </varlistentry>
 
       <varlistentry>
-        <term>-d <replaceable class="parameter">database</replaceable> </term>
+        <term>-t <replaceable class="parameter">datasrc_type</replaceable></term>
         <listitem><para>
-          Defines the filename for the database.
-	  The default is
-	  <filename>/usr/local/var/bind10-devel/zone.sqlite3</filename>.
-<!-- TODO: fix filename -->
+          Specifies the type of data source to store the zone.
+	  Currently, only the "sqlite3" type is supported (which is
+          the default of this option), which means the SQLite3 data
+          source.
         </para></listitem>
       </varlistentry>
 
       <varlistentry>
-        <term>-o <replaceable class="parameter">origin</replaceable></term>
+        <term>-C <replaceable class="parameter">zone_class</replaceable></term>
         <listitem><para>
-          Defines the default origin for the zone file records.
+          Specifies the RR class of the zone.
+	  Currently, only class IN is supported (which is the default
+          of this option) due to limitation of the underlying data
+          source implementation.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><replaceable class="parameter">zone name</replaceable></term>
+        <listitem><para>
+          The name of the zone to create or update.  This must be a valid DNS
+	  domain name.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><replaceable class="parameter">zone file</replaceable></term>
+        <listitem><para>
+          A path to the master zone file to be loaded.
         </para></listitem>
       </varlistentry>
 
@@ -131,8 +214,31 @@
   <refsect1>
     <title>AUTHORS</title>
     <para>
-      The <command>b10-loadzone</command> tool was initial written
-      by Evan Hunt of ISC.
+      A prior version of the <command>b10-loadzone</command> tool was
+      written by Evan Hunt of ISC.
+      The new version that this manual refers to was rewritten from
+      the scratch by the BIND 10 development team in around December 2012.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>BUGS</title>
+    <para>
+      As of the initial implementation, the underlying library that
+      this tool uses does not fully validate the loaded zone; for
+      example, loading will succeed even if it doesn't have the SOA or
+      NS record at its origin name.  Such checks will be implemented
+      in a near future version, but until then, the
+      <command>b10-loadzone</command> performs the existence of the
+      SOA and NS records by itself.  However, <command>b10-loadzone</command>
+      only warns about it, and does not cancel the load itself.
+      If this warning message is produced, it's the user's
+      responsibility to fix the errors and reload it.  When the
+      library is updated with the post load checks, it will be more
+      sophisticated and the such zone won't be successfully loaded.
+    </para>
+    <para>
+      There are some other issues noted in the DESCRIPTION section.
     </para>
   </refsect1>
 </refentry><!--

+ 342 - 0
src/bin/loadzone/loadzone.py.in

@@ -0,0 +1,342 @@
+#!@PYTHON@
+
+# 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 sys
+sys.path.append('@@PYTHONPATH@@')
+import time
+import signal
+from optparse import OptionParser
+from isc.dns import *
+from isc.datasrc import *
+import isc.util.process
+import isc.log
+from isc.log_messages.loadzone_messages import *
+
+isc.util.process.rename()
+
+# These are needed for logger settings
+import bind10_config
+import json
+from isc.config import module_spec_from_file
+from isc.config.ccsession import path_search
+
+isc.log.init("b10-loadzone")
+logger = isc.log.Logger("loadzone")
+
+# The default value for the interval of progress report in terms of the
+# number of RRs loaded in that interval.  Arbitrary choice, but intended to
+# be reasonably small to handle emergency exit.
+LOAD_INTERVAL_DEFAULT = 10000
+
+class BadArgument(Exception):
+    '''An exception indicating an error in command line argument.
+
+    '''
+    pass
+
+class LoadFailure(Exception):
+    '''An exception indicating failure in loading operation.
+
+    '''
+    pass
+
+def set_cmd_options(parser):
+    '''Helper function to set command-line options.
+
+    '''
+    parser.add_option("-c", "--datasrc-conf", dest="conf", action="store",
+                      help="""configuration of datasrc to load the zone in.
+Example: '{"database_file": "/path/to/dbfile/db.sqlite3"}'""",
+                      metavar='CONFIG')
+    parser.add_option("-d", "--debug", dest="debug_level",
+                      type='int', action="store", default=None,
+                      help="enable debug logs with the specified level [0-99]")
+    parser.add_option("-i", "--report-interval", dest="report_interval",
+                      type='int', action="store",
+                      default=LOAD_INTERVAL_DEFAULT,
+                      help="""report logs progress per specified number of RRs
+(specify 0 to suppress report) [default: %default]""")
+    parser.add_option("-t", "--datasrc-type", dest="datasrc_type",
+                      action="store", default='sqlite3',
+                      help="""type of data source (e.g., 'sqlite3')\n
+[default: %default]""")
+    parser.add_option("-C", "--class", dest="zone_class", action="store",
+                      default='IN',
+                      help="""RR class of the zone; currently must be 'IN'
+[default: %default]""")
+
+class LoadZoneRunner:
+    '''Main logic for the loadzone.
+
+    This is implemented as a class mainly for the convenience of tests.
+
+    '''
+    def __init__(self, command_args):
+        self.__command_args = command_args
+        self.__loaded_rrs = 0
+        self.__interrupted = False # will be set to True on receiving signal
+
+        # system-wide log configuration.  We need to configure logging this
+        # way so that the logging policy applies to underlying libraries, too.
+        self.__log_spec = json.dumps(isc.config.module_spec_from_file(
+                path_search('logging.spec', bind10_config.PLUGIN_PATHS)).
+                                     get_full_spec())
+        # "severity" and "debuglevel" are the tunable parameters, which will
+        # be set in _config_log().
+        self.__log_conf_base = {"loggers":
+                                    [{"name": "*",
+                                      "output_options":
+                                          [{"output": "stderr",
+                                            "destination": "console"}]}]}
+
+        # These are essentially private, and defined as "protected" for the
+        # convenience of tests inspecting them
+        self._zone_class = None
+        self._zone_name = None
+        self._zone_file = None
+        self._datasrc_config = None
+        self._datasrc_type = None
+        self._log_severity = 'INFO'
+        self._log_debuglevel = 0
+        self._report_interval = LOAD_INTERVAL_DEFAULT
+
+        self._config_log()
+
+    def _config_log(self):
+        '''Configure logging policy.
+
+        This is essentially private, but defined as "protected" for tests.
+
+        '''
+        self.__log_conf_base['loggers'][0]['severity'] = self._log_severity
+        self.__log_conf_base['loggers'][0]['debuglevel'] = self._log_debuglevel
+        isc.log.log_config_update(json.dumps(self.__log_conf_base),
+                                  self.__log_spec)
+
+    def _parse_args(self):
+        '''Parse command line options and other arguments.
+
+        This is essentially private, but defined as "protected" for tests.
+
+        '''
+
+        usage_txt = \
+            'usage: %prog [options] -c datasrc_config zonename zonefile'
+        parser = OptionParser(usage=usage_txt)
+        set_cmd_options(parser)
+        (options, args) = parser.parse_args(args=self.__command_args)
+
+        # Configure logging policy as early as possible
+        if options.debug_level is not None:
+            self._log_severity = 'DEBUG'
+            # optparse performs type check
+            self._log_debuglevel = int(options.debug_level)
+            if self._log_debuglevel < 0:
+                raise BadArgument(
+                    'Invalid debug level (must be non negative): %d' %
+                    self._log_debuglevel)
+        self._config_log()
+
+        self._datasrc_type = options.datasrc_type
+        self._datasrc_config = options.conf
+        if options.conf is None:
+            self._datasrc_config = self._get_datasrc_config(self._datasrc_type)
+        try:
+            self._zone_class = RRClass(options.zone_class)
+        except isc.dns.InvalidRRClass as ex:
+            raise BadArgument('Invalid zone class: ' + str(ex))
+        if self._zone_class != RRClass.IN():
+            raise BadArgument("RR class is not supported: " +
+                              str(self._zone_class))
+
+        self._report_interval = int(options.report_interval)
+        if self._report_interval < 0:
+            raise BadArgument(
+                'Invalid report interval (must be non negative): %d' %
+                self._report_interval)
+
+        if len(args) != 2:
+            raise BadArgument('Unexpected number of arguments: %d (must be 2)'
+                              % (len(args)))
+        try:
+            self._zone_name = Name(args[0])
+        except Exception as ex: # too broad, but there's no better granurality
+            raise BadArgument("Invalid zone name '" + args[0] + "': " +
+                              str(ex))
+        self._zone_file = args[1]
+
+    def _get_datasrc_config(self, datasrc_type):
+        ''''Return the default data source configuration of given type.
+
+        Right now, it only supports SQLite3, and hardcodes the syntax
+        of the default configuration.  It's a kind of workaround to balance
+        convenience of users and minimizing hardcoding of data source
+        specific logic in the entire tool.  In future this should be
+        more sophisticated.
+
+        This is essentially a private helper method for _parse_arg(),
+        but defined as "protected" so tests can use it directly.
+
+        '''
+        if datasrc_type != 'sqlite3':
+            raise BadArgument('default config is not available for ' +
+                              datasrc_type)
+
+        default_db_file = bind10_config.DATA_PATH + '/zone.sqlite3'
+        logger.info(LOADZONE_SQLITE3_USING_DEFAULT_CONFIG, default_db_file)
+        return '{"database_file": "' + default_db_file + '"}'
+
+    def __cancel_create(self):
+        '''sqlite3-only hack: delete the zone just created on load failure.
+
+        This should eventually be done via generic datasrc API, but right now
+        we don't have that interface.  Leaving the zone in this situation
+        is too bad, so we handle it with a workaround.
+
+        '''
+        if self._datasrc_type is not 'sqlite3':
+            return
+
+        import sqlite3          # we need the module only here
+        import json
+
+        # If we are here, the following should basically succeed; since
+        # this is considered a temporary workaround we don't bother to catch
+        # and recover rare failure cases.
+        dbfile = json.loads(self._datasrc_config)['database_file']
+        with sqlite3.connect(dbfile) as conn:
+            cur = conn.cursor()
+            cur.execute("DELETE FROM zones WHERE name = ?",
+                        [self._zone_name.to_text()])
+
+    def _report_progress(self, loaded_rrs):
+        '''Dump the current progress report to stdout.
+
+        This is essentially private, but defined as "protected" for tests.
+
+        '''
+        elapsed = time.time() - self.__start_time
+        sys.stdout.write("\r" + (80 * " "))
+        sys.stdout.write("\r%d RRs loaded in %.2f seconds" %
+                         (loaded_rrs, elapsed))
+
+    def _do_load(self):
+        '''Main part of the load logic.
+
+        This is essentially private, but defined as "protected" for tests.
+
+        '''
+        created = False
+        try:
+            datasrc_client = DataSourceClient(self._datasrc_type,
+                                              self._datasrc_config)
+            created = datasrc_client.create_zone(self._zone_name)
+            if created:
+                logger.info(LOADZONE_ZONE_CREATED, self._zone_name,
+                            self._zone_class)
+            else:
+                logger.info(LOADZONE_ZONE_UPDATING, self._zone_name,
+                            self._zone_class)
+            loader = ZoneLoader(datasrc_client, self._zone_name,
+                                self._zone_file)
+            self.__start_time = time.time()
+            if self._report_interval > 0:
+                limit = self._report_interval
+            else:
+                # Even if progress report is suppressed, we still load
+                # incrementally so we won't delay catching signals too long.
+                limit = LOAD_INTERVAL_DEFAULT
+            while (not self.__interrupted and
+                   not loader.load_incremental(limit)):
+                self.__loaded_rrs += self._report_interval
+                if self._report_interval > 0:
+                    self._report_progress(self.__loaded_rrs)
+            if self.__interrupted:
+                raise LoadFailure('loading interrupted by signal')
+
+            # On successful completion, add final '\n' to the progress
+            # report output (on failure don't bother to make it prettier).
+            if (self._report_interval > 0 and
+                self.__loaded_rrs >= self._report_interval):
+                sys.stdout.write('\n')
+        except Exception as ex:
+            # release any remaining lock held in the client/loader
+            loader, datasrc_client = None, None
+            if created:
+                self.__cancel_create()
+                logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
+                             self._zone_class)
+            raise LoadFailure(str(ex))
+
+    def _post_load_checks(self):
+        '''Perform minimal validity checks on the loaded zone.
+
+        We do this ourselves because the underlying library currently
+        doesn't do any checks.  Once the library support post-load validation
+        this check should be removed.
+
+        '''
+        datasrc_client = DataSourceClient(self._datasrc_type,
+                                          self._datasrc_config)
+        _, finder = datasrc_client.find_zone(self._zone_name) # should succeed
+        result = finder.find(self._zone_name, RRType.SOA())[0]
+        if result is not finder.SUCCESS:
+            self._post_load_warning('zone has no SOA')
+        result = finder.find(self._zone_name, RRType.NS())[0]
+        if result is not finder.SUCCESS:
+            self._post_load_warning('zone has no NS')
+
+    def _post_load_warning(self, msg):
+        logger.warn(LOADZONE_POSTLOAD_ISSUE, self._zone_name,
+                    self._zone_class, msg)
+
+    def _set_signal_handlers(self):
+        signal.signal(signal.SIGINT, self._interrupt_handler)
+        signal.signal(signal.SIGTERM, self._interrupt_handler)
+
+    def _interrupt_handler(self, signal, frame):
+        self.__interrupted = True
+
+    def run(self):
+        '''Top-level method, simply calling other helpers'''
+
+        try:
+            self._set_signal_handlers()
+            self._parse_args()
+            self._do_load()
+            total_elapsed_txt = "%.2f" % (time.time() - self.__start_time)
+            logger.info(LOADZONE_DONE, self.__loaded_rrs, self._zone_name,
+                        self._zone_class, total_elapsed_txt)
+            self._post_load_checks()
+            return 0
+        except BadArgument as ex:
+            logger.error(LOADZONE_ARGUMENT_ERROR, ex)
+        except LoadFailure as ex:
+            logger.error(LOADZONE_LOAD_ERROR, self._zone_name,
+                         self._zone_class, ex)
+        except Exception as ex:
+            logger.error(LOADZONE_UNEXPECTED_FAILURE, ex)
+        return 1
+
+if '__main__' == __name__:
+    runner = LoadZoneRunner(sys.argv[1:])
+    ret = runner.run()
+    sys.exit(ret)
+
+## Local Variables:
+## mode: python
+## End:

+ 81 - 0
src/bin/loadzone/loadzone_messages.mes

@@ -0,0 +1,81 @@
+# 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.
+
+# When you add a message to this file, it is a good idea to run
+# <topsrcdir>/tools/reorder_message_file.py to make sure the
+# messages are in the correct order.
+
+% LOADZONE_ARGUMENT_ERROR Error in command line arguments: %1
+Some semantics error in command line arguments or options to b10-loadzone
+is detected.  b10-loadzone does effectively nothing and immediately
+terminates.
+
+% LOADZONE_CANCEL_CREATE_ZONE Creation of new zone %1/%2 was canceled
+b10-loadzone has created a new zone in the data source (see
+LOADZONE_ZONE_CREATED), but the loading operation has subsequently
+failed.  The newly created zone has been removed from the data source,
+so that the data source will go back to the original state.
+
+% LOADZONE_DONE Loaded (at least) %1 RRs into zone %2/%3 in %4 seconds
+b10-loadzone has successfully loaded the specified zone.  If there was
+an old version of the zone in the data source, it is now deleted.
+It also prints (a lower bound of) the number of RRs that have been loaded
+and the time spent for the loading.  Due to a limitation of the
+current implementation of the underlying library however, it cannot show the
+exact number of the loaded RRs; it's counted for every N-th RR where N
+is the value of the -i command line option.  So, for smaller zones that
+don't even contain N RRs, the reported value will be 0.  This will be
+improved in a future version.
+
+% LOADZONE_LOAD_ERROR Failed to load zone %1/%2: %3
+Loading a zone by b10-loadzone fails for some reason in the middle of
+the loading.  This is most likely due to an error in the specified
+arguments to b10-loadzone (such as non-existent zone file) or an error
+in the zone file.  When this happens, the RRs loaded so far are
+effectively deleted from the zone, and the old version (if exists)
+will still remain valid for operations.
+
+% LOADZONE_POSTLOAD_ISSUE New version of zone %1/%2 has an issue: %3
+b10-loadzone detected a problem after a successful load of zone:
+either or both of SOA and NS records are missing at the zone origin.
+In the current implementation the load will not be canceled for such
+problems.  The operator will need to fix the issues and reload the
+zone; otherwise applications (such as b10-auth) that use this data
+source will not work as expected.
+
+% LOADZONE_SQLITE3_USING_DEFAULT_CONFIG Using default configuration with SQLite3 DB file %1
+The SQLite3 data source is specified as the data source type without a
+data source configuration.  b10-loadzone uses the default
+configuration with the default DB file for the BIND 10 system.
+
+% LOADZONE_UNEXPECTED_FAILURE Unexpected exception: %1
+b10-loadzone encounters an unexpected failure and terminates itself.
+This is generally a bug of b10-loadzone itself or the underlying
+data source library, so it's advisable to submit a bug report if
+this message is logged.  The incomplete attempt of loading should
+have been cleanly canceled in this case, too.
+
+% LOADZONE_ZONE_CREATED Zone %1/%2 does not exist in the data source, newly created
+The specified zone to b10-loadzone to load does not exist in the
+specified data source.  b10-loadzone has created a new empty zone
+in the data source.
+
+% LOADZONE_ZONE_UPDATING Started updating zone %1/%2 with removing old data (this can take a while)
+b10-loadzone started loading a new version of the zone as specified,
+beginning with removing the current contents of the zone (in a
+transaction, so the removal won't take effect until and unless the entire
+load is completed successfully).  If the old version of the zone is large,
+this can take time, such as a few minutes or more, without any visible
+feedback.  This is not a problem as long as the b10-loadzone process
+is working at a moderate load.

File diff suppressed because it is too large
+ 37 - 0
src/bin/loadzone/tests/Makefile.am


+ 4 - 1
src/bin/loadzone/tests/correct/Makefile.am

@@ -26,5 +26,8 @@ endif
 # TODO: maybe use TESTS?
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
-	echo Running test: correct_test.sh 
+	echo Running test: correct_test.sh
+	B10_FROM_SOURCE=$(abs_top_srcdir) \
+	B10_FROM_BUILD=$(abs_top_builddir) \
+	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/loadzone:$(abs_top_builddir)/src/lib/dns/python/.libs \
 	$(LIBRARY_PATH_PLACEHOLDER) $(SHELL) $(abs_builddir)/correct_test.sh

+ 9 - 9
src/bin/loadzone/tests/correct/correct_test.sh.in

@@ -18,7 +18,7 @@
 PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
 export PYTHON_EXEC
 
-PYTHONPATH=@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python
+PYTHONPATH=@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python:$PYTHONPATH
 export PYTHONPATH
 
 LOADZONE_PATH=@abs_top_builddir@/src/bin/loadzone
@@ -28,28 +28,28 @@ TEST_OUTPUT_PATH=@abs_top_builddir@/src/bin/loadzone//tests/correct
 status=0
 echo "Loadzone include. from include.db file"
 cd ${TEST_FILE_PATH}
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 include.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' include. include.db >> /dev/null
 
 echo "loadzone  ttl1. from ttl1.db file"
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 ttl1.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' ttl1. ttl1.db >> /dev/null
 
 echo "loadzone ttl2. from ttl2.db file"
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 ttl2.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' ttl2. ttl2.db >> /dev/null
 
 echo "loadzone mix1. from mix1.db"
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 mix1.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' mix1. mix1.db >> /dev/null
 
 echo "loadzone mix2. from mix2.db"
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 mix2.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' mix2. mix2.db >> /dev/null
 
 echo "loadzone ttlext. from ttlext.db"
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 ttlext.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' ttlext. ttlext.db >> /dev/null
 
 echo "loadzone example.com. from example.db"
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 example.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' example.com. example.db >> /dev/null
 
 echo "loadzone comment.example.com. from comment.db"
-${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 comment.db >> /dev/null
+${LOADZONE_PATH}/b10-loadzone -c '{"database_file": "'${TEST_OUTPUT_PATH}/zone.sqlite3'"}' comment.example.com. comment.db >> /dev/null
 
 echo "I:test master file \$INCLUDE semantics"
 echo "I:test master file BIND 8 compatibility TTL and \$TTL semantics"

+ 10 - 4
src/bin/loadzone/tests/correct/example.db

@@ -2,11 +2,17 @@
 $ORIGIN example.com.
 $TTL 60
 @    IN SOA   ns1.example.com. hostmaster.example.com. (1 43200 900 1814400 7200)
-     IN     20      NS  ns1
-                    NS  ns2
+; these need #2390
+;     IN     20      NS  ns1
+;                    NS  ns2
+     IN     20      NS  ns1.example.com.
+                    NS  ns2.example.com.
 ns1  IN     30      A   192.168.1.102
-            70      NS  ns3
-     IN             NS  ns4
+; these need #2390
+;            70      NS  ns3
+;     IN             NS  ns4
+            70      NS  ns3.example.com.
+     IN             NS  ns4.example.com.
      10     IN      MX  10  mail.example.com.
 ns2         80      A   1.1.1.1
 ns3  IN             A   2.2.2.2

+ 6 - 2
src/bin/loadzone/tests/correct/include.db

@@ -1,13 +1,17 @@
 $ORIGIN include.   ; initialize origin
 $TTL 300
-@			IN SOA	ns hostmaster (
+; this needs #2500
+;@			IN SOA	ns hostmaster (
+@			IN SOA	ns.include. hostmaster.include. (
 				1        ; serial
 				3600
 				1800
 				1814400
 				3600
 				)
-			NS	ns
+; this needs #2390
+;			NS	ns
+			NS	ns.include.
 
 ns			A	127.0.0.1
 

+ 6 - 2
src/bin/loadzone/tests/correct/mix1.db

@@ -1,12 +1,16 @@
 $ORIGIN mix1.
-@			IN SOA	ns hostmaster (
+; this needs #2500
+;@			IN SOA	ns hostmaster (
+@			IN SOA	ns.mix1. hostmaster.mix1. (
 				1        ; serial
 				3600
 				1800
 				1814400
 				3
 				)
-			NS	ns
+; this needs #2390
+;			NS	ns
+			NS	ns.mix1.
 ns			A	10.53.0.1
 a			TXT	"soa minttl 3"
 b		2	TXT	"explicit ttl 2"

+ 6 - 2
src/bin/loadzone/tests/correct/mix2.db

@@ -1,12 +1,16 @@
 $ORIGIN mix2.
-@		1	IN SOA	ns hostmaster (
+; this needs #2500
+;@		1	IN SOA	ns hostmaster (
+@		1	IN SOA	ns.mix2. hostmaster.mix2. (
 				1        ; serial
 				3600
 				1800
 				1814400
 				3
 				)
-			NS	ns
+; this needs #2390
+;			NS	ns
+			NS	ns.mix2.
 ns			A	10.53.0.1
 a			TXT	"inherited ttl 1"
 $INCLUDE mix2sub1.txt

+ 2 - 2
src/bin/loadzone/tests/correct/mix2sub2.txt

@@ -1,3 +1,3 @@
-f                       TXT     "default  ttl 3"
+f                       TXT     "default ttl 3"
 $TTL 5
-g                       TXT     "default  ttl 5"
+g                       TXT     "default ttl 5"

+ 6 - 2
src/bin/loadzone/tests/correct/ttl1.db

@@ -1,12 +1,16 @@
 $ORIGIN ttl1.
-@			IN SOA	ns hostmaster (
+; this needs #2500
+;@			IN SOA	ns hostmaster (
+@			IN SOA	ns.ttl1. hostmaster.ttl1. (
 				1        ; serial
 				3600
 				1800
 				1814400
 				3
 				)
-			NS	ns
+; this needs #2390
+;			NS	ns
+			NS	ns.ttl1.
 ns			A	10.53.0.1
 a			TXT	"soa minttl 3"
 b		2	TXT	"explicit ttl 2"

+ 6 - 2
src/bin/loadzone/tests/correct/ttl2.db

@@ -1,12 +1,16 @@
 $ORIGIN ttl2.
-@		1	IN SOA	ns hostmaster (
+; this needs #2500
+;@		1	IN SOA	ns hostmaster (
+@		1	IN SOA	ns.ttl2. hostmaster.ttl2 (
 				1        ; serial
 				3600
 				1800
 				1814400
 				3
 				)
-			NS	ns
+; this needs #2390
+;			NS	ns
+			NS	ns.ttl2.
 ns			A	10.53.0.1
 a			TXT	"inherited ttl 1"
 b		2	TXT	"explicit ttl 2"

+ 6 - 2
src/bin/loadzone/tests/correct/ttlext.db

@@ -1,12 +1,16 @@
 $ORIGIN ttlext.
-@			IN SOA	ns hostmaster (
+; this needs #2500
+;@			IN SOA	ns hostmaster (
+@			IN SOA	ns.ttlext. hostmaster.ttlext. (
 				1        ; serial
 				3600
 				1800
 				1814400
 				3
 				)
-			NS	ns
+; this needs #2390
+;			NS	ns
+			NS	ns.ttlext.
 ns			A	10.53.0.1
 a			TXT	"soa minttl 3"
 b		2S	TXT	"explicit ttl 2"

+ 0 - 1
src/bin/loadzone/tests/error/.gitignore

@@ -1 +0,0 @@
-/error_test.sh

+ 0 - 28
src/bin/loadzone/tests/error/Makefile.am

@@ -1,28 +0,0 @@
-EXTRA_DIST = error.known
-EXTRA_DIST += formerr1.db 
-EXTRA_DIST += formerr2.db
-EXTRA_DIST += formerr3.db
-EXTRA_DIST += formerr4.db
-EXTRA_DIST += formerr5.db
-EXTRA_DIST += include.txt
-EXTRA_DIST += keyerror1.db
-EXTRA_DIST += keyerror2.db
-EXTRA_DIST += keyerror3.db
-#EXTRA_DIST += nofilenane.db
-EXTRA_DIST += originerr1.db
-EXTRA_DIST += originerr2.db
-
-noinst_SCRIPTS = error_test.sh
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-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/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# TODO: use TESTS ?
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-	echo Running test: error_test.sh
-	$(LIBRARY_PATH_PLACEHOLDER) $(SHELL) $(abs_builddir)/error_test.sh

+ 0 - 11
src/bin/loadzone/tests/error/error.known

@@ -1,11 +0,0 @@
-Error reading zone file: Cannot parse RR, No $ORIGIN: @ IN SOA ns hostmaster 1 3600 1800 1814400 3600
-Error reading zone file: $ORIGIN is not absolute in record: $ORIGIN com
-Error reading zone file: Cannot parse RR: $TL 300
-Error reading zone file: Cannot parse RR: $OIGIN com.
-Error loading database: Error while loading com.: Cannot parse RR: $INLUDE file.txt
-Error loading database: Error while loading com.: Invalid $include format
-Error loading database: Error while loading com.: Cannot parse RR, No $ORIGIN:  include.txt sub
-Error reading zone file: Invalid TTL: ""
-Error reading zone file: Invalid TTL: "M"
-Error loading database: Error while loading com.: Cannot parse RR: b "no type error!"
-Error reading zone file: Could not open bogusfile

+ 0 - 82
src/bin/loadzone/tests/error/error_test.sh.in

@@ -1,82 +0,0 @@
-#! /bin/sh
-
-# Copyright (C) 2010  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.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-PYTHONPATH=@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python
-export PYTHONPATH
-
-LOADZONE_PATH=@abs_top_builddir@/src/bin/loadzone
-TEST_OUTPUT_PATH=@abs_top_builddir@/src/bin/loadzone/tests/error
-TEST_FILE_PATH=@abs_top_srcdir@/src/bin/loadzone/tests/error
-
-cd ${LOADZONE_PATH}/tests/error
-
-export LOADZONE_PATH
-status=0
-
-echo "PYTHON PATH: $PYTHONPATH"
-
-echo "Test no \$ORIGIN error in zone file"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/originerr1.db 1> /dev/null 2> error.out
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/originerr2.db 1> /dev/null 2>> error.out
-
-echo "Test: key word TTL spell error"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/keyerror1.db 1> /dev/null 2>> error.out
-
-echo "Test: key word ORIGIN spell error"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/keyerror2.db 1> /dev/null 2>> error.out
-
-echo "Test: key INCLUDE spell error"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/keyerror3.db 1> /dev/null 2>> error.out
-
-echo "Test: include formal error, miss filename"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr1.db 1> /dev/null 2>>error.out
-
-echo "Test: include form error, domain is not absolute"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr2.db 1> /dev/null 2>> error.out
-
-echo "Test: TTL form error, no ttl value"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr3.db 1> /dev/null 2>> error.out
-
-echo "Test: TTL form error, ttl value error"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr4.db 1> /dev/null 2>> error.out
-
-echo "Test: rr form error, no type"
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr5.db 1> /dev/null 2>> error.out
-
-echo "Test: zone file is bogus"
-# since bogusfile doesn't exist anyway, we *don't* specify the directory
-${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  bogusfile 1> /dev/null 2>> error.out
-
-diff error.out ${TEST_FILE_PATH}/error.known || status=1
-
-echo "Clean tmp file."
-rm -f error.out
-rm -f zone.sqlite3
-
-echo "I:exit status:$status"
-echo "-----------------------------------------------------------------------------"
-echo "Ran 11 test files"
-echo ""
-if [ "$status" -eq 1 ];then
-    echo "ERROR"
-else 
-    echo "OK"
-fi
-exit $status

+ 0 - 13
src/bin/loadzone/tests/error/formerr1.db

@@ -1,13 +0,0 @@
-$TTL 300
-$ORIGIN com.
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-$INCLUDE
-a			A	10.0.0.1

+ 0 - 12
src/bin/loadzone/tests/error/formerr2.db

@@ -1,12 +0,0 @@
-$TTL 300
-com.			IN SOA	ns.com. hostmaster.com. (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns.example.com.
-ns.com.			A	127.0.0.1
-$INCLUDE include.txt sub
-a.com.			A	10.0.0.1

+ 0 - 12
src/bin/loadzone/tests/error/formerr3.db

@@ -1,12 +0,0 @@
-$TTL 
-$ORIGIN com.
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-a			A	10.0.0.1

+ 0 - 12
src/bin/loadzone/tests/error/formerr4.db

@@ -1,12 +0,0 @@
-$TTL M
-$ORIGIN com.
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-a			A	10.0.0.1

+ 0 - 13
src/bin/loadzone/tests/error/formerr5.db

@@ -1,13 +0,0 @@
-$TTL 2M
-$ORIGIN com.
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1 ; ip value
-b               "no type error!"
-a			A	10.0.0.1

+ 0 - 1
src/bin/loadzone/tests/error/include.txt

@@ -1 +0,0 @@
-a  300 A 127.0.0.1

+ 0 - 12
src/bin/loadzone/tests/error/keyerror1.db

@@ -1,12 +0,0 @@
-$TL 300
-@ORIGIN com.
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-a			A	10.0.0.1

+ 0 - 12
src/bin/loadzone/tests/error/keyerror2.db

@@ -1,12 +0,0 @@
-$TTL 300
-$OIGIN com.
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-a			A	10.0.0.1

+ 0 - 13
src/bin/loadzone/tests/error/keyerror3.db

@@ -1,13 +0,0 @@
-$TTL 300
-$ORIGIN com.
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-$INLUDE file.txt
-a			A	10.0.0.1

+ 0 - 11
src/bin/loadzone/tests/error/originerr1.db

@@ -1,11 +0,0 @@
-$TTL 300
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-a			A	10.0.0.1

+ 0 - 12
src/bin/loadzone/tests/error/originerr2.db

@@ -1,12 +0,0 @@
-$TTL 300
-$ORIGIN com
-@			IN SOA	ns hostmaster (
-				1        ; serial
-				3600
-				1800
-				1814400
-				3600
-				)
-			NS	ns
-ns			A	127.0.0.1
-a			A	10.0.0.1

+ 342 - 0
src/bin/loadzone/tests/loadzone_test.py

@@ -0,0 +1,342 @@
+# 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.
+
+'''Tests for the loadzone module'''
+
+import unittest
+from loadzone import *
+from isc.dns import *
+from isc.datasrc import *
+import isc.log
+import bind10_config
+import os
+import shutil
+
+# Some common test parameters
+TESTDATA_PATH = os.environ['TESTDATA_PATH'] + os.sep
+READ_ZONE_DB_FILE = TESTDATA_PATH + "rwtest.sqlite3" # original, to be copied
+LOCAL_TESTDATA_PATH = os.environ['LOCAL_TESTDATA_PATH'] + os.sep
+READ_ZONE_DB_FILE = TESTDATA_PATH + "rwtest.sqlite3" # original, to be copied
+NEW_ZONE_TXT_FILE = LOCAL_TESTDATA_PATH + "example.org.zone"
+ALT_NEW_ZONE_TXT_FILE = TESTDATA_PATH + "example.com.zone"
+TESTDATA_WRITE_PATH = os.environ['TESTDATA_WRITE_PATH'] + os.sep
+WRITE_ZONE_DB_FILE = TESTDATA_WRITE_PATH + "rwtest.sqlite3.copied"
+TEST_ZONE_NAME = Name('example.org')
+DATASRC_CONFIG = '{"database_file": "' + WRITE_ZONE_DB_FILE + '"}'
+
+# before/after SOAs: different in mname and serial
+ORIG_SOA_TXT = 'example.org. 3600 IN SOA ns1.example.org. ' +\
+    'admin.example.org. 1234 3600 1800 2419200 7200\n'
+NEW_SOA_TXT = 'example.org. 3600 IN SOA ns.example.org. ' +\
+    'admin.example.org. 1235 3600 1800 2419200 7200\n'
+# This is the brandnew SOA for a newly created zone
+ALT_NEW_SOA_TXT = 'example.com. 3600 IN SOA ns.example.com. ' +\
+    'admin.example.com. 1234 3600 1800 2419200 7200\n'
+
+class TestLoadZoneRunner(unittest.TestCase):
+    def setUp(self):
+        shutil.copyfile(READ_ZONE_DB_FILE, WRITE_ZONE_DB_FILE)
+
+        # default command line arguments
+        self.__args = ['-c', DATASRC_CONFIG, 'example.org', NEW_ZONE_TXT_FILE]
+        self.__runner = LoadZoneRunner(self.__args)
+
+    def tearDown(self):
+        # Delete the used DB file; if some of the tests unexpectedly fail
+        # unexpectedly in the middle of updating the DB, a lock could stay
+        # there and would affect the other tests that would otherwise succeed.
+        os.unlink(WRITE_ZONE_DB_FILE)
+
+    def test_init(self):
+        '''
+        Checks initial class attributes
+        '''
+        self.assertIsNone(self.__runner._zone_class)
+        self.assertIsNone(self.__runner._zone_name)
+        self.assertIsNone(self.__runner._zone_file)
+        self.assertIsNone(self.__runner._datasrc_config)
+        self.assertIsNone(self.__runner._datasrc_type)
+        self.assertEqual(10000, self.__runner._report_interval)
+        self.assertEqual('INFO', self.__runner._log_severity)
+        self.assertEqual(0, self.__runner._log_debuglevel)
+
+    def test_parse_args(self):
+        self.__runner._parse_args()
+        self.assertEqual(TEST_ZONE_NAME, self.__runner._zone_name)
+        self.assertEqual(NEW_ZONE_TXT_FILE, self.__runner._zone_file)
+        self.assertEqual(DATASRC_CONFIG, self.__runner._datasrc_config)
+        self.assertEqual('sqlite3', self.__runner._datasrc_type) # default
+        self.assertEqual(10000, self.__runner._report_interval) # default
+        self.assertEqual(RRClass.IN(), self.__runner._zone_class) # default
+        self.assertEqual('INFO', self.__runner._log_severity) # default
+        self.assertEqual(0, self.__runner._log_debuglevel)
+
+    def test_set_loglevel(self):
+        runner = LoadZoneRunner(['-d', '1'] + self.__args)
+        runner._parse_args()
+        self.assertEqual('DEBUG', runner._log_severity)
+        self.assertEqual(1, runner._log_debuglevel)
+
+    def test_parse_bad_args(self):
+        # There must be exactly 2 non-option arguments: zone name and zone file
+        self.assertRaises(BadArgument, LoadZoneRunner([])._parse_args)
+        self.assertRaises(BadArgument, LoadZoneRunner(['example']).
+                          _parse_args)
+        self.assertRaises(BadArgument, LoadZoneRunner(self.__args + ['0']).
+                          _parse_args)
+
+        # Bad zone name
+        args = ['example.org', 'example.zone'] # otherwise valid args
+        self.assertRaises(BadArgument,
+                          LoadZoneRunner(['bad..name', 'example.zone'] + args).
+                          _parse_args)
+
+        # Bad class name
+        self.assertRaises(BadArgument,
+                          LoadZoneRunner(['-C', 'badclass'] + args).
+                          _parse_args)
+        # Unsupported class
+        self.assertRaises(BadArgument,
+                          LoadZoneRunner(['-C', 'CH'] + args)._parse_args)
+
+        # bad debug level
+        self.assertRaises(BadArgument,
+                          LoadZoneRunner(['-d', '-10'] + args)._parse_args)
+
+        # bad report interval
+        self.assertRaises(BadArgument,
+                          LoadZoneRunner(['-i', '-5'] + args)._parse_args)
+
+        # -c cannot be omitted unless it's type sqlite3 (right now)
+        self.assertRaises(BadArgument,
+                          LoadZoneRunner(['-t', 'memory'] + args)._parse_args)
+
+    def test_get_datasrc_config(self):
+        # For sqlite3, we use the config with the well-known DB file.
+        expected_conf = \
+            '{"database_file": "' + bind10_config.DATA_PATH + '/zone.sqlite3"}'
+        self.assertEqual(expected_conf,
+                         self.__runner._get_datasrc_config('sqlite3'))
+
+        # For other types, config must be given by hand for now
+        self.assertRaises(BadArgument, self.__runner._get_datasrc_config,
+                          'memory')
+
+    def __common_load_setup(self):
+        self.__runner._zone_class = RRClass.IN()
+        self.__runner._zone_name = TEST_ZONE_NAME
+        self.__runner._zone_file = NEW_ZONE_TXT_FILE
+        self.__runner._datasrc_type = 'sqlite3'
+        self.__runner._datasrc_config = DATASRC_CONFIG
+        self.__runner._report_interval = 1
+        self.__reports = []
+        self.__runner._report_progress = lambda x: self.__reports.append(x)
+
+    def __check_zone_soa(self, soa_txt, zone_name=TEST_ZONE_NAME):
+        """Check that the given SOA RR exists and matches the expected string
+
+        If soa_txt is None, the zone is expected to be non-existent.
+        Otherwise, if soa_txt is False, the zone should exist but SOA is
+        expected to be missing.
+
+        """
+
+        client = DataSourceClient('sqlite3', DATASRC_CONFIG)
+        result, finder = client.find_zone(zone_name)
+        if soa_txt is None:
+            self.assertEqual(client.NOTFOUND, result)
+            return
+        self.assertEqual(client.SUCCESS, result)
+        result, rrset, _ = finder.find(zone_name, RRType.SOA())
+        if soa_txt:
+            self.assertEqual(finder.SUCCESS, result)
+            self.assertEqual(soa_txt, rrset.to_text())
+        else:
+            self.assertEqual(finder.NXRRSET, result)
+
+    def test_load_update(self):
+        '''successful case to loading new contents to an existing zone.'''
+        self.__common_load_setup()
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.__runner._do_load()
+        # In this test setup every loaded RR will be reported, and there will
+        # be 3 RRs
+        self.assertEqual([1, 2, 3], self.__reports)
+        self.__check_zone_soa(NEW_SOA_TXT)
+
+    def test_load_update_skipped_report(self):
+        '''successful loading, with reports for every 2 RRs'''
+        self.__common_load_setup()
+        self.__runner._report_interval = 2
+        self.__runner._do_load()
+        self.assertEqual([2], self.__reports)
+
+    def test_load_update_no_report(self):
+        '''successful loading, without progress reports'''
+        self.__common_load_setup()
+        self.__runner._report_interval = 0
+        self.__runner._do_load()
+        self.assertEqual([], self.__reports) # no report
+        self.__check_zone_soa(NEW_SOA_TXT)   # but load is completed
+
+    def test_create_and_load(self):
+        '''successful case to loading contents to a new zone (created).'''
+        self.__common_load_setup()
+        self.__runner._zone_name = Name('example.com')
+        self.__runner._zone_file = ALT_NEW_ZONE_TXT_FILE
+        self.__check_zone_soa(None, zone_name=Name('example.com'))
+        self.__runner._do_load()
+        self.__check_zone_soa(ALT_NEW_SOA_TXT, zone_name=Name('example.com'))
+
+    def test_load_fail_badconfig(self):
+        '''Load attempt fails due to broken datasrc config.'''
+        self.__common_load_setup()
+        self.__runner._datasrc_config = "invalid config"
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.assertRaises(LoadFailure, self.__runner._do_load)
+        self.__check_zone_soa(ORIG_SOA_TXT) # no change to the zone
+
+    def test_load_fail_badzone(self):
+        '''Load attempt fails due to broken zone file.'''
+        self.__common_load_setup()
+        self.__runner._zone_file = \
+            LOCAL_TESTDATA_PATH + '/broken-example.org.zone'
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.assertRaises(LoadFailure, self.__runner._do_load)
+        self.__check_zone_soa(ORIG_SOA_TXT)
+
+    def test_load_fail_noloader(self):
+        '''Load attempt fails because loading isn't supported'''
+        self.__common_load_setup()
+        self.__runner._datasrc_type = 'memory'
+        self.__runner._datasrc_config = '{"type": "memory"}'
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.assertRaises(LoadFailure, self.__runner._do_load)
+        self.__check_zone_soa(ORIG_SOA_TXT)
+
+    def test_load_fail_create_cancel(self):
+        '''Load attempt fails and new creation of zone is canceled'''
+        self.__common_load_setup()
+        self.__runner._zone_name = Name('example.com')
+        self.__runner._zone_file = 'no-such-file'
+        self.__check_zone_soa(None, zone_name=Name('example.com'))
+        self.assertRaises(LoadFailure, self.__runner._do_load)
+        # _do_load() should have once created the zone but then canceled it.
+        self.__check_zone_soa(None, zone_name=Name('example.com'))
+
+    def __common_post_load_setup(self, zone_file):
+        '''Common setup procedure for post load tests.'''
+        # replace the LoadZoneRunner's original _post_load_warning() for
+        # inspection
+        self.__warnings = []
+        self.__runner._post_load_warning = \
+            lambda msg: self.__warnings.append(msg)
+
+        # perform load and invoke checks
+        self.__common_load_setup()
+        self.__runner._zone_file = zone_file
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.__runner._do_load()
+        self.__runner._post_load_checks()
+
+    def test_load_post_check_fail_soa(self):
+        '''Load succeeds but warns about missing SOA, should cause warn'''
+        self.__common_load_setup()
+        self.__common_post_load_setup(LOCAL_TESTDATA_PATH +
+                                      '/example-nosoa.org.zone')
+        self.__check_zone_soa(False)
+        self.assertEqual(1, len(self.__warnings))
+        self.assertEqual('zone has no SOA', self.__warnings[0])
+
+    def test_load_post_check_fail_ns(self):
+        '''Load succeeds but warns about missing NS, should cause warn'''
+        self.__common_load_setup()
+        self.__common_post_load_setup(LOCAL_TESTDATA_PATH +
+                                      '/example-nons.org.zone')
+        self.__check_zone_soa(NEW_SOA_TXT)
+        self.assertEqual(1, len(self.__warnings))
+        self.assertEqual('zone has no NS', self.__warnings[0])
+
+    def __interrupt_progress(self, loaded_rrs):
+        '''A helper emulating a signal in the middle of loading.
+
+        On the second progress report, it internally invokes the signal
+        handler to see if it stops the loading.
+
+        '''
+        self.__reports.append(loaded_rrs)
+        if len(self.__reports) == 2:
+            self.__runner._interrupt_handler()
+
+    def test_load_interrupted(self):
+        '''Load attempt fails due to signal interruption'''
+        self.__common_load_setup()
+        self.__runner._report_progress = lambda x: self.__interrupt_progress(x)
+        # The interrupting _report_progress() will terminate the loading
+        # in the middle.  the number of reports is smaller, and the zone
+        # won't be changed.
+        self.assertRaises(LoadFailure, self.__runner._do_load)
+        self.assertEqual([1, 2], self.__reports)
+        self.__check_zone_soa(ORIG_SOA_TXT)
+
+    def test_load_interrupted_create_cancel(self):
+        '''Load attempt for a new zone fails due to signal interruption
+
+        It cancels the zone creation.
+
+        '''
+        self.__common_load_setup()
+        self.__runner._report_progress = lambda x: self.__interrupt_progress(x)
+        self.__runner._zone_name = Name('example.com')
+        self.__runner._zone_file = ALT_NEW_ZONE_TXT_FILE
+        self.__check_zone_soa(None, zone_name=Name('example.com'))
+        self.assertRaises(LoadFailure, self.__runner._do_load)
+        self.assertEqual([1, 2], self.__reports)
+        self.__check_zone_soa(None, zone_name=Name('example.com'))
+
+    def test_run_success(self):
+        '''Check for the top-level method.
+
+        Detailed behavior is tested in other tests.  We only check the
+        return value of run(), and the zone is successfully loaded.
+
+        '''
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.assertEqual(0, self.__runner.run())
+        self.__check_zone_soa(NEW_SOA_TXT)
+
+    def test_run_fail(self):
+        '''Check for the top-level method, failure case.
+
+        Similar to the success test, but loading will fail, and return
+        value should be 1.
+
+        '''
+        runner = LoadZoneRunner(['-c', DATASRC_CONFIG, 'example.org',
+                                 LOCAL_TESTDATA_PATH +
+                                 '/broken-example.org.zone'])
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.assertEqual(1, runner.run())
+        self.__check_zone_soa(ORIG_SOA_TXT)
+
+if __name__== "__main__":
+    isc.log.resetUnitTestRootLogger()
+    # Disable the internal logging setup so the test output won't be too
+    # verbose by default.
+    LoadZoneRunner._config_log = lambda x: None
+
+    # Cancel signal handlers so we can stop tests when they hang
+    LoadZoneRunner._set_signal_handlers = lambda x: None
+    unittest.main()

+ 11 - 0
src/bin/loadzone/tests/testdata/broken-example.org.zone

@@ -0,0 +1,11 @@
+example.org.    3600    IN  SOA (
+		ns.example.org.
+		admin.example.org.
+		1235
+		3600		;1H
+		1800		;30M
+		2419200
+		7200)
+example.org.    3600    IN  NS ns.example.org.
+ns.example.org.	3600    IN  A 192.0.2.1
+bad..name.example.org. 3600 IN AAAA 2001:db8::1

+ 10 - 0
src/bin/loadzone/tests/testdata/example-nons.org.zone

@@ -0,0 +1,10 @@
+;; Intentionally missing SOA for testing post-load checks
+example.org.    3600    IN  SOA (
+		ns.example.org.
+		admin.example.org.
+		1235
+		3600		;1H
+		1800		;30M
+		2419200
+		7200)
+ns.example.org.	3600    IN  A 192.0.2.1

+ 3 - 0
src/bin/loadzone/tests/testdata/example-nosoa.org.zone

@@ -0,0 +1,3 @@
+;; Intentionally missing SOA for testing post-load checks
+example.org.    3600    IN  NS ns.example.org.
+ns.example.org.	3600    IN  A 192.0.2.1

+ 10 - 0
src/bin/loadzone/tests/testdata/example.org.zone

@@ -0,0 +1,10 @@
+example.org.    3600    IN  SOA (
+		ns.example.org.
+		admin.example.org.
+		1235
+		3600		;1H
+		1800		;30M
+		2419200
+		7200)
+example.org.    3600    IN  NS ns.example.org.
+ns.example.org.	3600    IN  A 192.0.2.1

+ 13 - 4
src/lib/dns/master_loader.cc

@@ -79,7 +79,7 @@ public:
         warn_rfc1035_ttl_(true)
     {}
 
-    void pushSource(const std::string& filename) {
+    void pushSource(const std::string& filename, const Name& current_origin) {
         std::string error;
         if (!lexer_.pushSource(filename.c_str(), &error)) {
             if (initialized_) {
@@ -91,7 +91,7 @@ public:
             }
         }
         // Store the current status, so we can recover it upon popSource
-        include_info_.push_back(IncludeInfo(active_origin_, last_name_));
+        include_info_.push_back(IncludeInfo(current_origin, last_name_));
         initialized_ = true;
         previous_name_ = false;
     }
@@ -182,9 +182,18 @@ private:
             filename(lexer_.getNextToken(MasterToken::QSTRING).getString());
 
         // There optionally can be an origin, that applies before the include.
+        // We need to save the currently active origin before calling
+        // doOrigin(), because it would update active_origin_ while we need
+        // to pass the active origin before recognizing the new origin to
+        // pushSource.  Note: RFC 1035 is not really clear on this: it reads
+        // "regardless of changes... within the included file", but the new
+        // origin is not really specified "within the included file".
+        // Nevertheless, this behavior is probably more likely to be the
+        // intent of the RFC, and it's compatible with BIND 9.
+        const Name current_origin = active_origin_;
         doOrigin(true);
 
-        pushSource(filename);
+        pushSource(filename, current_origin);
     }
 
     // A helper method for loadIncremental(). It parses part of an RR
@@ -512,7 +521,7 @@ MasterLoader::MasterLoaderImpl::loadIncremental(size_t count_limit) {
                   "Trying to load when already loaded");
     }
     if (!initialized_) {
-        pushSource(master_file_);
+        pushSource(master_file_, active_origin_);
     }
     size_t count = 0;
     while (ok_ && count < count_limit) {

+ 4 - 3
src/lib/dns/tests/master_loader_unittest.cc

@@ -544,7 +544,7 @@ TEST_F(MasterLoaderTest, includeAndOrigin) {
         "@  1H  IN  A   192.0.2.1\n"
         // Then include the file with data and switch origin back
         "$INCLUDE " TEST_DATA_SRCDIR "/example.org example.org.\n"
-        // Another RR to see the switch survives after we exit include
+        // Another RR to see we fall back to the previous origin.
         "www    1H  IN  A   192.0.2.1\n";
     stringstream ss(include_string);
     setLoader(ss, Name("example.org"), RRClass::IN(),
@@ -557,7 +557,7 @@ TEST_F(MasterLoaderTest, includeAndOrigin) {
     // And check it's the correct data
     checkARR("www.example.org");
     checkBasicRRs();
-    checkARR("www.example.org");
+    checkARR("www.www.example.org");
 }
 
 // Like above, but the origin after include is bogus. The whole line should
@@ -582,7 +582,8 @@ TEST_F(MasterLoaderTest, includeAndBadOrigin) {
 
 // Check the origin doesn't get outside of the included file.
 TEST_F(MasterLoaderTest, includeOriginRestore) {
-    const string include_string = "$INCLUDE " TEST_DATA_SRCDIR "/origincheck.txt\n"
+    const string include_string =
+        "$INCLUDE " TEST_DATA_SRCDIR "/origincheck.txt\n"
         "@  1H  IN  A   192.0.2.1\n";
     stringstream ss(include_string);
     setLoader(ss, Name("example.org"), RRClass::IN(),

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

@@ -2,7 +2,7 @@ SUBDIRS = . tests
 
 # old data, should be removed in the near future once conversion is done
 pythondir = $(pyexecdir)/isc/datasrc
-python_PYTHON = __init__.py master.py sqlite3_ds.py
+python_PYTHON = __init__.py sqlite3_ds.py
 
 
 # new data

+ 0 - 616
src/lib/python/isc/datasrc/master.py

@@ -1,616 +0,0 @@
-# Copyright (C) 2010  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 sys, re, string
-import time
-import os
-#########################################################################
-# define exceptions
-#########################################################################
-class MasterFileError(Exception):
-    pass
-
-#########################################################################
-# pop: remove the first word from a line
-# input: a line
-# returns: first word, rest of the line
-#########################################################################
-def pop(line):
-    list = line.split()
-    first, rest = '', ''
-    if len(list) != 0:
-        first = list[0]
-    if len(list) > 1:
-        rest = ' '.join(list[1:])
-    return first, rest
-
-#########################################################################
-# cleanup: removes excess content from zone file data, including comments
-# and extra whitespace
-# input:
-#   line of text
-# returns:
-#   the same line, with comments removed, leading and trailing
-#   whitespace removed, and all other whitespace compressed to
-#   single spaces
-#########################################################################
-decomment = re.compile('^\s*((?:[^;"]|"[^"]*")*)\s*(?:|;.*)$')
-# Regular expression explained:
-# First, ignore any whitespace at the start. Then take the content,
-# each bit is either a harmless character (no ; nor ") or a string -
-# sequence between " " not containing double quotes. Then there may
-# be a comment at the end.
-def cleanup(s):
-    global decomment
-    s = s.strip().expandtabs()
-    s = decomment.search(s).group(1)
-    return ' '.join(s.split())
-
-#########################################################################
-# istype: check whether a string is a known RR type.
-# returns: boolean
-#########################################################################
-rrtypes = set(['a', 'aaaa', 'afsdb', 'apl', 'cert', 'cname', 'dhcid',
-               'dlv', 'dname', 'dnskey', 'ds', 'gpos', 'hinfo', 'hip',
-               'ipseckey', 'isdn', 'key', 'kx', 'loc', 'mb', 'md',
-               'mf', 'mg', 'minfo', 'mr', 'mx', 'naptr', 'ns', 'nsap',
-               'nsap-ptr', 'nsec', 'nsec3', 'nsec3param', 'null',
-               'nxt', 'opt', 'ptr', 'px', 'rp', 'rrsig', 'rt', 'sig',
-               'soa', 'spf', 'srv', 'sshfp', 'tkey', 'tsig', 'txt',
-               'x25', 'wks'])
-def istype(s):
-    global rrtypes
-    if s.lower() in rrtypes:
-        return True
-    else:
-        return False
-
-#########################################################################
-# isclass: check whether a string is a known RR class.  (only 'IN' is
-# supported, but the others must still be recognizable.)
-# returns: boolean
-#########################################################################
-rrclasses = set(['in', 'ch', 'chaos', 'hs', 'hesiod'])
-def isclass(s):
-    global rrclasses
-    if s.lower() in rrclasses:
-        return True
-    else:
-        return False
-
-#########################################################################
-# isname: check whether a string is a valid DNS name.
-# returns: boolean
-#########################################################################
-name_regex = re.compile('[-\w\$\d\/*]+(?:\.[-\w\$\d\/]+)*\.?')
-def isname(s):
-    global name_regex
-    if s == '.' or name_regex.match(s):
-        return True
-    else:
-        return False
-
-#########################################################################
-# isttl: check whether a string is a valid TTL specifier.
-# returns: boolean
-#########################################################################
-ttl_regex = re.compile('([0-9]+[wdhms]?)+$', re.I)
-def isttl(s):
-    global ttl_regex
-    if ttl_regex.match(s):
-        return True
-    else:
-        return False
-
-#########################################################################
-# parse_ttl: convert a TTL field into an integer TTL value
-# (multiplying as needed for minutes, hours, etc.)
-# input:
-#   string
-# returns:
-#   int
-# throws:
-#   MasterFileError
-#########################################################################
-def parse_ttl(s):
-    sum = 0
-    if not isttl(s):
-        raise MasterFileError('Invalid TTL: ' + s)
-    for ttl_expr in re.findall('\d+[wdhms]?', s, re.I):
-        if ttl_expr.isdigit():
-            ttl = int(ttl_expr)
-            sum += ttl
-            continue
-        ttl = int(ttl_expr[:-1])
-        suffix = ttl_expr[-1].lower()
-        if suffix == 'w':
-            ttl *= 604800
-        elif suffix == 'd':
-            ttl *= 86400
-        elif suffix == 'h':
-            ttl *= 3600
-        elif suffix == 'm':
-            ttl *= 60
-        sum += ttl
-    return str(sum)
-
-#########################################################################
-# records: generator function to return complete RRs from the zone file,
-# combining lines when necessary because of parentheses
-# input:
-#   descriptor for a zone master file (returned from openzone)
-# yields:
-#   complete RR
-#########################################################################
-def records(input):
-    record = []
-    complete = True
-    paren = 0
-    size = 0
-    for line in input:
-        size += len(line)
-        list = cleanup(line).split()
-        for word in list:
-            if paren == 0:
-                left, p, right = word.partition('(')
-                if p == '(':
-                    if left: record.append(left)
-                    if right: record.append(right)
-                    paren += 1
-                else:
-                    record.append(word)
-            else:
-                left, p, right = word.partition(')')
-                if p == ')':
-                    if left: record.append(left)
-                    if right: record.append(right)
-                    paren -= 1
-                else:
-                    record.append(word)
-
-        if paren == 1 or not record:
-            continue
-
-        ret = ' '.join(record)
-        record = []
-        oldsize = size
-        size = 0
-        yield ret, oldsize
-
-#########################################################################
-# define the MasterFile class for reading zone master files
-#########################################################################
-class MasterFile:
-    __rrclass = 'IN'
-    __maxttl = 0x7fffffff
-    __ttl = ''
-    __lastttl = ''
-    __zonefile = ''
-    __name = ''
-    __file_level = 0
-    __file_type = ""
-    __init_time = time.time()
-    __records_num = 0
-
-    def __init__(self, filename, initial_origin = '', verbose = False):
-        self.__initial_origin = initial_origin
-        self.__origin = initial_origin
-        self.__datafile = filename
-
-        try:
-            self.__zonefile = open(filename, 'r')
-        except:
-            raise MasterFileError("Could not open " + filename)
-        self.__filesize = os.fstat(self.__zonefile.fileno()).st_size
-
-        self.__cur = 0
-        self.__numback = 0
-        self.__verbose = verbose
-        try:
-            self.__zonefile = open(filename, 'r')
-        except:
-            raise MasterFileError("Could not open " + filename)
-
-    def __status(self):
-        interval = time.time() - MasterFile.__init_time
-        if self.__filesize == 0:
-            percent = 100
-        else:
-            percent = (self.__cur * 100)/self.__filesize
-
-        sys.stdout.write("\r" + (80 * " "))
-        sys.stdout.write("\r%d RR(s) loaded in %.2f second(s) (%.2f%% of %s%s)"\
-                % (MasterFile.__records_num, interval, percent, MasterFile.__file_type, self.__datafile))
-
-    def __del__(self):
-        if self.__zonefile:
-            self.__zonefile.close()
-    ########################################################################
-    # check if the zonename is relative
-    # no then return
-    # yes , sets the relative domain name to the stated name
-    #######################################################################
-    def __statedname(self, name, record):
-        if name[-1] != '.':
-            if not self.__origin:
-                raise MasterFileError("Cannot parse RR, No $ORIGIN: " + record)
-            elif self.__origin == '.':
-                name += '.'
-            else:
-                name += '.' + self.__origin
-        return name
-    #####################################################################
-    # handle $ORIGIN, $TTL and $GENERATE directives
-    # (currently only $ORIGIN and $TTL are implemented)
-    # input:
-    #   a line from a zone file
-    # returns:
-    #   a boolean indicating whether a directive was found
-    # throws:
-    #   MasterFileError
-    #########################################################################
-    def __directive(self, s):
-        first, more = pop(s)
-        second, more = pop(more)
-        if re.match('\$origin', first, re.I):
-            if not second or not isname(second):
-                raise MasterFileError('Invalid $ORIGIN')
-            if more:
-                raise MasterFileError('Invalid $ORIGIN')
-            if second[-1] == '.':
-                self.__origin = second
-            elif not self.__origin:
-                raise MasterFileError("$ORIGIN is not absolute in record: %s" % s)
-            elif self.__origin != '.':
-                self.__origin = second + '.' + self.__origin
-            else:
-                self.__origin = second + '.'
-            return True
-        elif re.match('\$ttl', first, re.I):
-            if not second or not isttl(second):
-                raise MasterFileError('Invalid TTL: "' + second + '"')
-            if more:
-                raise MasterFileError('Invalid $TTL statement')
-            MasterFile.__ttl = parse_ttl(second)
-            if int(MasterFile.__ttl) > self.__maxttl:
-                raise MasterFileError('TTL too high: ' + second)
-            return True
-        elif re.match('\$generate', first, re.I):
-            raise MasterFileError('$GENERATE not yet implemented')
-        else:
-            return False
-
-    #########################################################################
-    # handle $INCLUDE directives
-    # input:
-    #   a line from a zone file
-    # returns:
-    #   the parsed output of the included file, if any, or an empty array
-    # throws:
-    #   MasterFileError
-    #########################################################################
-    __include_syntax1 = re.compile('\s+(\S+)(?:\s+(\S+))?$', re.I)
-    __include_syntax2 = re.compile('\s+"([^"]+)"(?:\s+(\S+))?$', re.I)
-    __include_syntax3 = re.compile("\s+'([^']+)'(?:\s+(\S+))?$", re.I)
-    def __include(self, s):
-        if not s.lower().startswith('$include'):
-            return "", ""
-        s = s[len('$include'):]
-        m = self.__include_syntax1.match(s)
-        if not m:
-            m = self.__include_syntax2.match(s)
-        if not m:
-            m = self.__include_syntax3.match(s)
-        if not m:
-            raise MasterFileError('Invalid $include format')
-        file = m.group(1)
-        if m.group(2):
-            if not isname(m.group(2)):
-                raise MasterFileError('Invalid $include format (invalid origin)')
-            origin = self.__statedname(m.group(2), s)
-        else:
-            origin = self.__origin
-        return file, origin
-
-    #########################################################################
-    # try parsing an RR on the assumption that the type is specified in
-    # field 4, and name, ttl and class are in fields 1-3
-    # are all specified, with type in field 4
-    # input:
-    #   a record to parse, and the most recent name found in prior records
-    # returns:
-    #   empty list if parse failed, else name, ttl, class, type, rdata
-    #########################################################################
-    def __four(self, record, curname):
-        ret = ''
-        list = record.split()
-        if len(list) <= 4:
-            return ret
-        if istype(list[3]):
-            if isclass(list[2]) and isttl(list[1]) and isname(list[0]):
-                name, ttl, rrclass, rrtype = list[0:4]
-                ttl = parse_ttl(ttl)
-                MasterFile.__lastttl = ttl or MasterFile.__lastttl
-                rdata = ' '.join(list[4:])
-                ret = name, ttl, rrclass, rrtype, rdata
-            elif isclass(list[1]) and isttl(list[2]) and isname(list[0]):
-                name, rrclass, ttl, rrtype = list[0:4]
-                ttl = parse_ttl(ttl)
-                MasterFile.__lastttl = ttl or MasterFile.__lastttl
-                rdata = ' '.join(list[4:])
-                ret = name, ttl, rrclass, rrtype, rdata
-        return ret
-
-    #########################################################################
-    # try parsing an RR on the assumption that the type is specified
-    # in field 3, and one of name, ttl, or class has been omitted
-    # input:
-    #   a record to parse, and the most recent name found in prior records
-    # returns:
-    #   empty list if parse failed, else name, ttl, class, type, rdata
-    #########################################################################
-    def __getttl(self):
-        return MasterFile.__ttl or MasterFile.__lastttl
-
-    def __three(self, record, curname):
-        ret = ''
-        list = record.split()
-        if len(list) <= 3:
-            return ret
-        if istype(list[2]) and not istype(list[1]):
-            if isclass(list[1]) and not isttl(list[0]) and isname(list[0]):
-                rrclass = list[1]
-                ttl = self.__getttl()
-                name = list[0]
-            elif not isclass(list[1]) and isttl(list[1]) and not isclass(list[0]) and isname(list[0]):
-                rrclass = self.__rrclass
-                ttl = parse_ttl(list[1])
-                MasterFile.__lastttl = ttl or MasterFile.__lastttl
-                name = list[0]
-            elif curname and isclass(list[1]) and isttl(list[0]):
-                rrclass = list[1]
-                ttl = parse_ttl(list[0])
-                MasterFile.__lastttl = ttl or MasterFile.__lastttl
-                name = curname
-            elif curname and isttl(list[1]) and isclass(list[0]):
-                rrclass = list[0]
-                ttl = parse_ttl(list[1])
-                MasterFile.__lastttl = ttl or MasterFile.__lastttl
-                name = curname
-            else:
-                return ret
-            rrtype = list[2]
-            rdata = ' '.join(list[3:])
-            ret = name, ttl, rrclass, rrtype, rdata
-        return ret
-
-    #########################################################################
-    # try parsing an RR on the assumption that the type is specified in
-    # field 2, and field 1 is either name or ttl
-    # input:
-    #   a record to parse, and the most recent name found in prior records
-    # returns:
-    #   empty list if parse failed, else name, ttl, class, type, rdata
-    # throws:
-    #   MasterFileError
-    #########################################################################
-    def __two(self, record, curname):
-        ret = ''
-        list = record.split()
-        if len(list) <= 2:
-            return ret
-        if istype(list[1]):
-            rrclass = self.__rrclass
-            rrtype = list[1]
-            if list[0].lower() == 'rrsig':
-                name = curname
-                ttl = self.__getttl()
-                rrtype = list[0]
-                rdata = ' '.join(list[1:])
-            elif isttl(list[0]):
-                ttl = parse_ttl(list[0])
-                name = curname
-                rdata = ' '.join(list[2:])
-            elif isclass(list[0]):
-                ttl = self.__getttl()
-                name = curname
-                rdata = ' '.join(list[2:])
-            elif isname(list[0]):
-                name = list[0]
-                ttl = self.__getttl()
-                rdata = ' '.join(list[2:])
-            else:
-                raise MasterFileError("Cannot parse RR: " + record)
-
-            ret = name, ttl, rrclass, rrtype, rdata
-        return ret
-
-    ########################################################################
-    #close verbose
-    ######################################################################
-    def closeverbose(self):
-        self.__status()
-
-    #########################################################################
-    # zonedata: generator function to parse a zone master file and return
-    # each RR as a (name, ttl, type, class, rdata) tuple
-    #########################################################################
-    def zonedata(self):
-        name = ''
-        last_status = 0.0
-        flag = 1
-
-        for record, size in records(self.__zonefile):
-            if self.__verbose:
-                now = time.time()
-                if flag == 1:
-                    self.__status()
-                    flag = 0
-                if now - last_status >= 1.0:
-                    self.__status()
-                    last_status = now
-
-            self.__cur += size
-            if self.__directive(record):
-                continue
-
-            incl, suborigin = self.__include(record)
-            if incl:
-                if self.__filesize == 0:
-                    percent = 100
-                else:
-                    percent = (self.__cur * 100)/self.__filesize
-                if self.__verbose:
-                    sys.stdout.write("\r" + (80 * " "))
-                    sys.stdout.write("\rIncluding \"%s\" from \"%s\"\n" % (incl, self.__datafile))
-                MasterFile.__file_level += 1
-                MasterFile.__file_type = "included "
-                sub = MasterFile(incl, suborigin, self.__verbose)
-
-                for rrname, ttl, rrclass, rrtype, rdata in sub.zonedata():
-                    yield (rrname, ttl, rrclass, rrtype, rdata)
-                if self.__verbose:
-                    sub.closeverbose()
-                MasterFile.__file_level -= 1
-                if MasterFile.__file_level == 0:
-                    MasterFile.__file_type = ""
-                del sub
-                continue
-
-            # replace @ with origin
-            rl = record.split()
-            if rl[0] == '@':
-                rl[0] = self.__origin
-                if not self.__origin:
-                    raise MasterFileError("Cannot parse RR, No $ORIGIN: " + record)
-                record = ' '.join(rl)
-
-            result = self.__four(record, name)
-
-            if not result:
-                result = self.__three(record, name)
-
-            if not result:
-                result = self.__two(record, name)
-
-            if not result:
-                first, rdata = pop(record)
-                if istype(first):
-                    result = name, self.__getttl(), self.__rrclass, first, rdata
-
-            if not result:
-                raise MasterFileError("Cannot parse RR: " + record)
-
-            name, ttl, rrclass, rrtype, rdata = result
-            name = self.__statedname(name, record)
-
-            if rrclass.lower() != 'in':
-                raise MasterFileError("CH and HS zones not supported")
-
-            # add origin to rdata containing names, if necessary
-            if rrtype.lower() in ('cname', 'dname', 'ns', 'ptr'):
-                if not isname(rdata):
-                    raise MasterFileError("Invalid " + rrtype + ": " + rdata)
-                rdata = self.__statedname(rdata, record)
-
-            if rrtype.lower() == 'soa':
-                soa = rdata.split()
-                if len(soa) < 2 or not isname(soa[0]) or not isname(soa[1]):
-                    raise MasterFileError("Invalid " + rrtype + ": " + rdata)
-                soa[0] = self.__statedname(soa[0], record)
-                soa[1] = self.__statedname(soa[1], record)
-                if not MasterFile.__ttl and not ttl:
-                    MasterFile.__ttl = MasterFile.__ttl or parse_ttl(soa[-1])
-                    ttl = MasterFile.__ttl
-
-                for index in range(3, len(soa)):
-                    if isttl(soa[index]):
-                        soa[index] = parse_ttl(soa[index])
-                    else :
-                        raise MasterFileError("No TTL specified; in soa record!")
-                rdata = ' '.join(soa)
-
-            if not ttl:
-                raise MasterFileError("No TTL specified; zone rejected")
-
-            if rrtype.lower() == 'mx':
-                mx = rdata.split()
-                if len(mx) != 2 or not isname(mx[1]):
-                    raise MasterFileError("Invalid " + rrtype + ": " + rdata)
-                if mx[1][-1] != '.':
-                    mx[1] += '.' + self.__origin
-                    rdata = ' '.join(mx)
-            MasterFile.__records_num += 1
-            yield (name, ttl, rrclass, rrtype, rdata)
-
-    #########################################################################
-    # zonename: scans zone data for an SOA record, returns its name, restores
-    # the zone file to its prior state
-    #########################################################################
-    def zonename(self):
-        if self.__name:
-            return self.__name
-        old_origin = self.__origin
-        self.__origin = self.__initial_origin
-        cur_value = self.__cur
-        old_location = self.__zonefile.tell()
-        old_verbose = self.__verbose
-        self.__verbose = False
-        self.__zonefile.seek(0)
-
-        for name, ttl, rrclass, rrtype, rdata in self.zonedata():
-            if rrtype.lower() == 'soa':
-                break
-        self.__zonefile.seek(old_location)
-        self.__origin = old_origin
-        self.__cur = cur_value
-        if rrtype.lower() != 'soa':
-            raise MasterFileError("No SOA found")
-        self.__name = name
-        self.__verbose = old_verbose
-        return name
-
-    #########################################################################
-    # reset: reset the state of the master file
-    #########################################################################
-    def reset(self):
-        self.__zonefile.seek(0)
-        self.__origin = self.__initial_origin
-        MasterFile.__ttl = ''
-        MasterFile.__lastttl = ''
-
-#########################################################################
-# main: used for testing; parse a zone file and print out each record
-# broken up into separate name, ttl, class, type, and rdata files
-#########################################################################
-def main():
-    try:
-        file = sys.argv[1]
-    except:
-        file = 'testfile'
-    master = MasterFile(file, '.')
-    print ('zone name: ' + master.zonename())
-    print ('---------------------')
-    for name, ttl, rrclass, rrtype, rdata in master.zonedata():
-        print ('name: ' + name)
-        print ('ttl: ' + ttl)
-        print ('rrclass: ' + rrclass)
-        print ('rrtype: ' + rrtype)
-        print ('rdata: ' + rdata)
-        print ('---------------------')
-    del master
-
-if __name__ == "__main__":
-    main()

+ 1 - 2
src/lib/python/isc/datasrc/tests/Makefile.am

@@ -1,6 +1,4 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-# old tests, TODO remove or change to use new API?
-#PYTESTS = master_test.py
 PYTESTS =  datasrc_test.py sqlite3_ds_test.py
 PYTESTS += clientlist_test.py zone_loader_test.py
 EXTRA_DIST = $(PYTESTS)
@@ -29,6 +27,7 @@ LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/data
 endif
 
 # test using command-line arguments, so use check-local target instead of TESTS
+# We need to define B10_FROM_BUILD for datasrc loadable modules
 check-local:
 if ENABLE_PYTHON_COVERAGE
 	touch $(abs_top_srcdir)/.coverage

+ 0 - 35
src/lib/python/isc/datasrc/tests/master_test.py

@@ -1,35 +0,0 @@
-# Copyright (C) 2010  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.datasrc.master import *
-import unittest
-
-class TestTTL(unittest.TestCase):
-    def test_ttl(self):
-        self.assertTrue(isttl('3600'))
-        self.assertTrue(isttl('1W'))
-        self.assertTrue(isttl('1w'))
-        self.assertTrue(isttl('2D'))
-        self.assertTrue(isttl('2d'))
-        self.assertTrue(isttl('30M'))
-        self.assertTrue(isttl('30m'))
-        self.assertTrue(isttl('10S'))
-        self.assertTrue(isttl('10s'))
-        self.assertTrue(isttl('2W1D'))
-        self.assertFalse(isttl('not a ttl'))
-        self.assertFalse(isttl('1X'))
-
-if __name__ == '__main__':
-    unittest.main()

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

@@ -14,6 +14,7 @@ EXTRA_DIST += config_messages.py
 EXTRA_DIST += notify_out_messages.py
 EXTRA_DIST += libddns_messages.py
 EXTRA_DIST += libxfrin_messages.py
+EXTRA_DIST += loadzone_messages.py
 EXTRA_DIST += server_common_messages.py
 EXTRA_DIST += dbutil_messages.py
 
@@ -31,6 +32,7 @@ CLEANFILES += config_messages.pyc
 CLEANFILES += notify_out_messages.pyc
 CLEANFILES += libddns_messages.pyc
 CLEANFILES += libxfrin_messages.pyc
+CLEANFILES += loadzone_messages.pyc
 CLEANFILES += server_common_messages.pyc
 CLEANFILES += dbutil_messages.pyc
 

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

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

+ 2 - 2
tests/system/bindctl/setup.sh

@@ -22,5 +22,5 @@ SUBTEST_TOP=${TEST_TOP}/bindctl
 cp ${SUBTEST_TOP}/nsx1/b10-config.db.template ${SUBTEST_TOP}/nsx1/b10-config.db
 
 rm -f ${SUBTEST_TOP}/*/zone.sqlite3
-${B10_LOADZONE} -o . -d ${SUBTEST_TOP}/nsx1/zone.sqlite3 \
-	${SUBTEST_TOP}//nsx1/root.db
+${B10_LOADZONE} -c '{"database_file": "'${SUBTEST_TOP}/nsx1/zone.sqlite3'"}' \
+	. ${SUBTEST_TOP}//nsx1/root.db