Parcourir la source

[2964] added -e (empty zone) option to b10-loadzone.

it'll soon be impossible to assume a specific data source for the very
initial transfer, we need to extract the zone creation part from b10-xfrin
(which should be the right course anyway).  the new loadzone option is
my proposed way to do this.  I think this option itself is of some use, too.
JINMEI Tatuya il y a 12 ans
Parent
commit
43c9101c5e

+ 34 - 0
src/bin/loadzone/b10-loadzone.xml

@@ -52,6 +52,12 @@
       <arg choice="req">zone name</arg>
       <arg choice="req">zone file</arg>
     </cmdsynopsis>
+    <cmdsynopsis>
+      <command>b10-loadzone</command>
+      <arg><option>-e</option></arg>
+      <arg><option>other options</option></arg>
+      <arg choice="req">zone name</arg>
+    </cmdsynopsis>
   </refsynopsisdiv>
 
   <refsect1>
@@ -61,6 +67,10 @@
       in a BIND 10 ready data source format.
       Master files are text files that contain DNS Resource Records
       in text form.
+      This utility can also be used to empty current content, if any,
+      of the specified zone from the specified data source so the
+      existence of the zone is recognized in the data source without
+      any content (resource records).
     </para>
     <note><simpara>Currently only the SQLITE3 data source is supported.
     </simpara></note>
@@ -104,6 +114,19 @@
       old version will still remain accessible for other applications.
     </para>
 
+    <para>
+      If the <command>-e</command> command line option is specified,
+      <command>b10-loadzone</command> does not take the zone name
+      argument.
+      In this case it empties any existing content of the specified
+      zone from the specified data source while the data source still
+      recognizes the existence of the zone.  If the zone originally
+      didn't exist in the zone, it effectively creates the zone without
+      any content.
+      This mode of operation can be used for setting up a secondary
+      zone before transferring zone content from a primary server.
+    </para>
+
   </refsect1>
 
   <refsect1>
@@ -142,6 +165,17 @@
       </varlistentry>
 
       <varlistentry>
+        <term>-e</term>
+        <listitem><para>
+	    Empty any existing zone content instead of loading new one.
+	    When this option is specified, the zone file command line
+	    argument must not be provided.
+	    The <command>-i</command> option has no effect, but it
+	    does not cause a failure; it will be simply ignored.
+        </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

+ 50 - 14
src/bin/loadzone/loadzone.py.in

@@ -66,6 +66,8 @@ Example: '{"database_file": "/path/to/dbfile/db.sqlite3"}'""",
     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("-e", "--empty", dest="empty_zone",
+                      action="store_true", help="empty zone content (no load)")
     parser.add_option("-i", "--report-interval", dest="report_interval",
                       type='int', action="store",
                       default=LOAD_INTERVAL_DEFAULT,
@@ -113,6 +115,7 @@ class LoadZoneRunner:
         self._datasrc_type = None
         self._log_severity = 'INFO'
         self._log_debuglevel = 0
+        self._empty_zone = False
         self._report_interval = LOAD_INTERVAL_DEFAULT
         self._start_time = None
         # This one will be used in (rare) cases where we want to allow tests to
@@ -140,7 +143,8 @@ class LoadZoneRunner:
         '''
 
         usage_txt = \
-            'usage: %prog [options] -c datasrc_config zonename zonefile'
+            'usage: %prog [options] -c datasrc_config zonename zonefile\n' + \
+            '       %prog [options] -c datasrc_config -e zonename'
         parser = OptionParser(usage=usage_txt)
         set_cmd_options(parser)
         (options, args) = parser.parse_args(args=self.__command_args)
@@ -174,15 +178,22 @@ class LoadZoneRunner:
                 '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)))
+        if options.empty_zone:
+            self._empty_zone = True
+
+        # Check number of non option arguments: must be 1 with -e; 2 otherwise.
+        num_args = 1 if self._empty_zone else 2
+
+        if len(args) != num_args:
+            raise BadArgument('Unexpected number of arguments: %d (must be %d)'
+                              % (len(args), num_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]
+        if len(args) > 1:
+            self._zone_file = args[1]
 
     def _get_datasrc_config(self, datasrc_type):
         ''''Return the default data source configuration of given type.
@@ -254,6 +265,34 @@ class LoadZoneRunner:
             else:
                 logger.info(LOADZONE_ZONE_UPDATING, self._zone_name,
                             self._zone_class)
+            if self._empty_zone:
+                self.__make_empty_zone(datasrc_client)
+            else:
+                self.__load_from_file(datasrc_client)
+        except Exception as ex:
+            if created:
+                datasrc_client.delete_zone(self._zone_name)
+                logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
+                             self._zone_class)
+            raise LoadFailure(str(ex))
+
+    def __make_empty_zone(self, datasrc_client):
+        """Subroutine of _do_load(), create an empty zone or make it empty."""
+        try:
+            updater = datasrc_client.get_updater(self._zone_name, True)
+            updater.commit()
+            logger.info(LOADZONE_EMPTY_DONE, self._zone_name,
+                        self._zone_class)
+        except Exception:
+            # once updater is created, it's very unlikely that commit() fails,
+            # but in case it happens, clear updater to release any remaining
+            # lock.
+            updater = None
+            raise
+
+    def __load_from_file(self, datasrc_client):
+        """Subroutine of _do_load(), load a zone file into data source."""
+        try:
             loader = ZoneLoader(datasrc_client, self._zone_name,
                                 self._zone_file)
             self._start_time = time.time()
@@ -279,14 +318,14 @@ class LoadZoneRunner:
                 sys.stdout.write('\n')
             # record the final count of the loaded RRs for logging
             self._loaded_rrs = loader.get_rr_count()
-        except Exception as ex:
+
+            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)
+        except Exception:
             # release any remaining lock held in the loader
             loader = None
-            if created:
-                datasrc_client.delete_zone(self._zone_name)
-                logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
-                             self._zone_class)
-            raise LoadFailure(str(ex))
+            raise
 
     def _set_signal_handlers(self):
         signal.signal(signal.SIGINT, self._interrupt_handler)
@@ -302,9 +341,6 @@ class LoadZoneRunner:
             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)
             return 0
         except BadArgument as ex:
             logger.error(LOADZONE_ARGUMENT_ERROR, ex)

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

@@ -33,6 +33,11 @@ an old version of the zone in the data source, it is now deleted.
 It also prints the number of RRs that have been loaded
 and the time spent for the loading.
 
+% LOADZONE_EMPTY_DONE Completed emptying zone %1/%2
+b10-loadzone has successfully emptied content of the specified zone.
+This includes the case where the content didn't previously exist, in which
+case it just still reamins empty.
+
 % 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

+ 31 - 6
src/bin/loadzone/tests/loadzone_test.py

@@ -82,6 +82,7 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.assertEqual(RRClass.IN, self.__runner._zone_class) # default
         self.assertEqual('INFO', self.__runner._log_severity) # default
         self.assertEqual(0, self.__runner._log_debuglevel)
+        self.assertFalse(self.__runner._empty_zone)
 
     def test_set_loglevel(self):
         runner = LoadZoneRunner(['-d', '1'] + self.__args)
@@ -90,13 +91,19 @@ class TestLoadZoneRunner(unittest.TestCase):
         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
+        # There must usually 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)
 
+        # With -e it must be only zone name
+        self.assertRaises(BadArgument, LoadZoneRunner(
+                ['-e', 'example', 'example.zone'])._parse_args)
+        self.assertRaises(BadArgument, LoadZoneRunner(['-e'])._parse_args)
+
         # Bad zone name
         args = ['example.org', 'example.zone'] # otherwise valid args
         self.assertRaises(BadArgument,
@@ -134,22 +141,24 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.assertRaises(BadArgument, self.__runner._get_datasrc_config,
                           'memory')
 
-    def __common_load_setup(self):
+    def __common_load_setup(self, empty=False):
         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.__runner._empty_zone = empty
         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.
+        If soa_txt is None, the zone is expected to be non-existent;
+        if it's 'empty', the zone should exist but is expected to be empty;
+        if soa_txt is False, the zone should exist but SOA is expected to be
+        missing.
 
         """
 
@@ -160,7 +169,10 @@ class TestLoadZoneRunner(unittest.TestCase):
             return
         self.assertEqual(client.SUCCESS, result)
         result, rrset, _ = finder.find(zone_name, RRType.SOA)
-        if soa_txt:
+        if soa_txt == 'empty':
+            self.assertEqual(finder.NXDOMAIN, result)
+            self.assertIsNone(rrset)
+        elif soa_txt:
             self.assertEqual(finder.SUCCESS, result)
             self.assertEqual(soa_txt, rrset.to_text())
         else:
@@ -269,6 +281,19 @@ class TestLoadZoneRunner(unittest.TestCase):
         # _do_load() should have once created the zone but then canceled it.
         self.__check_zone_soa(None, zone_name=Name('example.com'))
 
+    def test_create_and_empty(self):
+        self.__common_load_setup(True)
+        self.__runner._zone_name = Name('example.com')
+        self.__check_zone_soa(None, zone_name=Name('example.com'))
+        self.__runner._do_load()
+        self.__check_zone_soa('empty', zone_name=Name('example.com'))
+
+    def test_empty(self):
+        self.__common_load_setup(True)
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.__runner._do_load()
+        self.__check_zone_soa('empty')
+
     def __common_post_load_setup(self, zone_file):
         '''Common setup procedure for post load tests which should fail.'''
         # replace the LoadZoneRunner's original _post_load_warning() for