Parcourir la source

[master] Merge branch 'trac2158'

Conflicts:
	ChangeLog
Naoki Kambe il y a 12 ans
Parent
commit
e68c127fed

+ 7 - 0
ChangeLog

@@ -1,3 +1,10 @@
+475.	[func]		naokikambe
+	Added Xfrout statistics counters: notifyoutv4, notifyoutv6, xfrrej, and
+	xfrreqdone. These are per-zone type counters. The value of these
+	counters can be seen with zone name by invoking "Stats show Xfrout" via
+	bindctl.
+	(Trac #2158, git TBD)
+
 474.	[func]      stephen
 474.	[func]      stephen
 	DHCP servers now use the BIND 10 logging system for messages.
 	DHCP servers now use the BIND 10 logging system for messages.
 	(Trac #1545, git de69a92613b36bd3944cb061e1b7c611c3c85506)
 	(Trac #1545, git de69a92613b36bd3944cb061e1b7c611c3c85506)

+ 48 - 0
src/bin/xfrout/b10-xfrout.xml

@@ -153,6 +153,54 @@
 
 
   </refsect1>
   </refsect1>
 
 
+  <refsect1>
+    <title>STATISTICS DATA</title>
+
+    <para>
+      The statistics data collected by the <command>b10-xfrout</command>
+      daemon for <quote>Xfrout</quote> include:
+    </para>
+
+    <variablelist>
+
+      <varlistentry>
+        <term>notifyoutv4</term>
+        <listitem><simpara>
+	 Number of IPv4 notifies per zone name sent out from Xfrout
+	</simpara></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>notifyoutv6</term>
+        <listitem><simpara>
+	 Number of IPv6 notifies per zone name sent out from Xfrout
+	</simpara></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>xfrrej</term>
+        <listitem><simpara>
+         Number of XFR requests per zone name rejected by Xfrout
+        </simpara></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>xfrreqdone</term>
+        <listitem><simpara>
+	 Number of requested zone transfers per zone name completed
+        </simpara></listitem>
+      </varlistentry>
+
+    </variablelist>
+
+    <para>
+      In per-zone counters the special zone name '_SERVER_' exists. It doesn't
+      mean a specific zone. It represents an entire server and its value means
+      a total count of all zones.
+    </para>
+
+  </refsect1>
+
 <!--
 <!--
   <refsect1>
   <refsect1>
     <title>OPTIONS</title>
     <title>OPTIONS</title>

+ 146 - 1
src/bin/xfrout/tests/xfrout_test.py.in

@@ -277,13 +277,23 @@ class TestXfroutSessionBase(unittest.TestCase):
                                        # When not testing ACLs, simply accept
                                        # When not testing ACLs, simply accept
                                        isc.acl.dns.REQUEST_LOADER.load(
                                        isc.acl.dns.REQUEST_LOADER.load(
                                            [{"action": "ACCEPT"}]),
                                            [{"action": "ACCEPT"}]),
-                                       {})
+                                       {},
+                                       counter_xfrrej=self._counter_xfrrej,
+                                       counter_xfrreqdone=self._counter_xfrreqdone)
         self.set_request_type(RRType.AXFR()) # test AXFR by default
         self.set_request_type(RRType.AXFR()) # test AXFR by default
         self.mdata = self.create_request_data()
         self.mdata = self.create_request_data()
         self.soa_rrset = create_soa(SOA_CURRENT_VERSION)
         self.soa_rrset = create_soa(SOA_CURRENT_VERSION)
         # some test replaces a module-wide function.  We should ensure the
         # some test replaces a module-wide function.  We should ensure the
         # original is used elsewhere.
         # original is used elsewhere.
         self.orig_get_rrset_len = xfrout.get_rrset_len
         self.orig_get_rrset_len = xfrout.get_rrset_len
+        self._zone_name_xfrrej = None
+        self._zone_name_xfrreqdone = None
+
+    def _counter_xfrrej(self, zone_name):
+        self._zone_name_xfrrej = zone_name
+
+    def _counter_xfrreqdone(self, zone_name):
+        self._zone_name_xfrreqdone = zone_name
 
 
     def tearDown(self):
     def tearDown(self):
         xfrout.get_rrset_len = self.orig_get_rrset_len
         xfrout.get_rrset_len = self.orig_get_rrset_len
@@ -458,7 +468,28 @@ class TestXfroutSession(TestXfroutSessionBase):
         # ACL checks only with the default ACL
         # ACL checks only with the default ACL
         def acl_setter(acl):
         def acl_setter(acl):
             self.xfrsess._acl = acl
             self.xfrsess._acl = acl
+        self.assertIsNone(self._zone_name_xfrrej)
+        self.check_transfer_acl(acl_setter)
+        self.assertEqual(self._zone_name_xfrrej, TEST_ZONE_NAME_STR)
+
+    def test_transfer_acl_with_nonetype_xfrrej(self):
+        # ACL checks only with the default ACL and NoneType xfrrej
+        # counter
+        def acl_setter(acl):
+            self.xfrsess._acl = acl
+        self.xfrsess._counter_xfrrej = None
+        self.assertIsNone(self._zone_name_xfrrej)
         self.check_transfer_acl(acl_setter)
         self.check_transfer_acl(acl_setter)
+        self.assertIsNone(self._zone_name_xfrrej)
+
+    def test_transfer_acl_with_notcallable_xfrrej(self):
+        # ACL checks only with the default ACL and not callable xfrrej
+        # counter
+        def acl_setter(acl):
+            self.xfrsess._acl = acl
+        self.xfrsess._counter_xfrrej = 'NOT CALLABLE'
+        self.assertRaises(TypeError,
+                          self.check_transfer_acl, acl_setter)
 
 
     def test_transfer_zoneacl(self):
     def test_transfer_zoneacl(self):
         # ACL check with a per zone ACL + default ACL.  The per zone ACL
         # ACL check with a per zone ACL + default ACL.  The per zone ACL
@@ -469,7 +500,9 @@ class TestXfroutSession(TestXfroutSessionBase):
             self.xfrsess._zone_config[zone_key]['transfer_acl'] = acl
             self.xfrsess._zone_config[zone_key]['transfer_acl'] = acl
             self.xfrsess._acl = isc.acl.dns.REQUEST_LOADER.load([
             self.xfrsess._acl = isc.acl.dns.REQUEST_LOADER.load([
                     {"from": "127.0.0.1", "action": "DROP"}])
                     {"from": "127.0.0.1", "action": "DROP"}])
+        self.assertIsNone(self._zone_name_xfrrej)
         self.check_transfer_acl(acl_setter)
         self.check_transfer_acl(acl_setter)
+        self.assertEqual(self._zone_name_xfrrej, TEST_ZONE_NAME_STR)
 
 
     def test_transfer_zoneacl_nomatch(self):
     def test_transfer_zoneacl_nomatch(self):
         # similar to the previous one, but the per zone doesn't match the
         # similar to the previous one, but the per zone doesn't match the
@@ -481,7 +514,9 @@ class TestXfroutSession(TestXfroutSessionBase):
                 isc.acl.dns.REQUEST_LOADER.load([
                 isc.acl.dns.REQUEST_LOADER.load([
                     {"from": "127.0.0.1", "action": "DROP"}])
                     {"from": "127.0.0.1", "action": "DROP"}])
             self.xfrsess._acl = acl
             self.xfrsess._acl = acl
+        self.assertIsNone(self._zone_name_xfrrej)
         self.check_transfer_acl(acl_setter)
         self.check_transfer_acl(acl_setter)
+        self.assertEqual(self._zone_name_xfrrej, TEST_ZONE_NAME_STR)
 
 
     def test_get_transfer_acl(self):
     def test_get_transfer_acl(self):
         # set the default ACL.  If there's no specific zone ACL, this one
         # set the default ACL.  If there's no specific zone ACL, this one
@@ -831,9 +866,39 @@ class TestXfroutSession(TestXfroutSessionBase):
         def myreply(msg, sock):
         def myreply(msg, sock):
             self.sock.send(b"success")
             self.sock.send(b"success")
 
 
+        self.assertIsNone(self._zone_name_xfrreqdone)
         self.xfrsess._reply_xfrout_query = myreply
         self.xfrsess._reply_xfrout_query = myreply
         self.xfrsess.dns_xfrout_start(self.sock, self.mdata)
         self.xfrsess.dns_xfrout_start(self.sock, self.mdata)
         self.assertEqual(self.sock.readsent(), b"success")
         self.assertEqual(self.sock.readsent(), b"success")
+        self.assertEqual(self._zone_name_xfrreqdone, TEST_ZONE_NAME_STR)
+
+    def test_dns_xfrout_start_with_nonetype_xfrreqdone(self):
+        def noerror(msg, name, rrclass):
+            return Rcode.NOERROR()
+        self.xfrsess._xfrout_setup = noerror
+
+        def myreply(msg, sock):
+            self.sock.send(b"success")
+
+        self.assertIsNone(self._zone_name_xfrreqdone)
+        self.xfrsess._reply_xfrout_query = myreply
+        self.xfrsess._counter_xfrreqdone = None
+        self.xfrsess.dns_xfrout_start(self.sock, self.mdata)
+        self.assertIsNone(self._zone_name_xfrreqdone)
+
+    def test_dns_xfrout_start_with_notcallable_xfrreqdone(self):
+        def noerror(msg, name, rrclass):
+            return Rcode.NOERROR()
+        self.xfrsess._xfrout_setup = noerror
+
+        def myreply(msg, sock):
+            self.sock.send(b"success")
+
+        self.xfrsess._reply_xfrout_query = myreply
+        self.xfrsess._counter_xfrreqdone = 'NOT CALLABLE'
+        self.assertRaises(TypeError,
+                          self.xfrsess.dns_xfrout_start, self.sock,
+                          self.mdata)
 
 
     def test_reply_xfrout_query_axfr(self):
     def test_reply_xfrout_query_axfr(self):
         self.xfrsess._soa = self.soa_rrset
         self.xfrsess._soa = self.soa_rrset
@@ -1153,6 +1218,7 @@ class MyUnixSockServer(UnixSockServer):
         self._common_init()
         self._common_init()
         self._cc = MyCCSession()
         self._cc = MyCCSession()
         self.update_config_data(self._cc.get_full_config())
         self.update_config_data(self._cc.get_full_config())
+        self._counters = {}
 
 
 class TestUnixSockServer(unittest.TestCase):
 class TestUnixSockServer(unittest.TestCase):
     def setUp(self):
     def setUp(self):
@@ -1504,6 +1570,80 @@ class MyXfroutServer(XfroutServer):
         self._unix_socket_server = None
         self._unix_socket_server = None
         # Disable the wait for threads
         # Disable the wait for threads
         self._wait_for_threads = lambda : None
         self._wait_for_threads = lambda : None
+        self._cc.get_module_spec = lambda:\
+            isc.config.module_spec_from_file(xfrout.SPECFILE_LOCATION)
+        # setup an XfroutCount object
+        self._counter = XfroutCounter(
+            self._cc.get_module_spec().get_statistics_spec())
+
+class TestXfroutCounter(unittest.TestCase):
+    def setUp(self):
+        statistics_spec = \
+            isc.config.module_spec_from_file(\
+            xfrout.SPECFILE_LOCATION).get_statistics_spec()
+        self.xfrout_counter = XfroutCounter(statistics_spec)
+        self._counters = isc.config.spec_name_list(\
+            isc.config.find_spec_part(\
+                statistics_spec, XfroutCounter.perzone_prefix)\
+                ['named_set_item_spec']['map_item_spec'])
+        self._started = threading.Event()
+        self._number = 3 # number of the threads
+        self._cycle = 10000 # number of counting per thread
+
+    def test_get_default_statistics_data(self):
+        self.assertEqual(self.xfrout_counter._get_default_statistics_data(),
+                         {XfroutCounter.perzone_prefix: {
+                            XfroutCounter.entire_server: \
+                              dict([(cnt, 0) for cnt in self._counters])
+                         }})
+
+    def setup_incrementer(self, incrementer):
+        self._started.wait()
+        for i in range(self._cycle): incrementer(TEST_ZONE_NAME_STR)
+
+    def start_incrementer(self, incrementer):
+        threads = []
+        for i in range(self._number):
+            threads.append(threading.Thread(\
+                    target=self.setup_incrementer,\
+                        args=(incrementer,)\
+                        ))
+        for th in threads: th.start()
+        self._started.set()
+        for th in threads: th.join()
+
+    def get_count(self, zone_name, counter_name):
+        return isc.cc.data.find(\
+            self.xfrout_counter.get_statistics(),\
+                '%s/%s/%s' % (XfroutCounter.perzone_prefix,\
+                                  zone_name, counter_name))
+
+    def test_incrementers(self):
+        result = { XfroutCounter.entire_server: {},
+                   TEST_ZONE_NAME_STR: {} }
+        for counter_name in self._counters:
+                incrementer = getattr(self.xfrout_counter, 'inc_%s' % counter_name)
+                self.start_incrementer(incrementer)
+                self.assertEqual(self.get_count(\
+                            TEST_ZONE_NAME_STR, counter_name), \
+                                     self._number * self._cycle)
+                self.assertEqual(self.get_count(\
+                        XfroutCounter.entire_server, counter_name), \
+                                     self._number * self._cycle)
+                result[XfroutCounter.entire_server][counter_name] = \
+                    result[TEST_ZONE_NAME_STR][counter_name] = \
+                    self._number * self._cycle
+        self.assertEqual(
+            self.xfrout_counter.get_statistics(),
+            {XfroutCounter.perzone_prefix: result})
+
+    def test_add_perzone_counter(self):
+        for counter_name in self._counters:
+            self.assertRaises(isc.cc.data.DataNotFoundError,\
+                                  self.get_count, TEST_ZONE_NAME_STR, counter_name)
+        self.xfrout_counter._add_perzone_counter(TEST_ZONE_NAME_STR)
+        for counter_name in self._counters:
+            self.assertEqual(self.get_count(TEST_ZONE_NAME_STR, counter_name), 0)
 
 
 class TestXfroutServer(unittest.TestCase):
 class TestXfroutServer(unittest.TestCase):
     def setUp(self):
     def setUp(self):
@@ -1514,6 +1654,11 @@ class TestXfroutServer(unittest.TestCase):
         self.assertTrue(self.xfrout_server._notifier.shutdown_called)
         self.assertTrue(self.xfrout_server._notifier.shutdown_called)
         self.assertTrue(self.xfrout_server._cc.stopped)
         self.assertTrue(self.xfrout_server._cc.stopped)
 
 
+    def test_getstats(self):
+        self.assertEqual(
+            self.xfrout_server.command_handler('getstats', None), \
+                create_answer(0,  {}))
+
 if __name__== "__main__":
 if __name__== "__main__":
     isc.log.resetUnitTestRootLogger()
     isc.log.resetUnitTestRootLogger()
     unittest.main()
     unittest.main()

+ 143 - 9
src/bin/xfrout/xfrout.py.in

@@ -153,7 +153,8 @@ def get_soa_serial(soa_rdata):
 
 
 class XfroutSession():
 class XfroutSession():
     def __init__(self, sock_fd, request_data, server, tsig_key_ring, remote,
     def __init__(self, sock_fd, request_data, server, tsig_key_ring, remote,
-                 default_acl, zone_config, client_class=DataSourceClient):
+                 default_acl, zone_config, client_class=DataSourceClient,
+                 counter_xfrrej=None, counter_xfrreqdone=None):
         self._sock_fd = sock_fd
         self._sock_fd = sock_fd
         self._request_data = request_data
         self._request_data = request_data
         self._server = server
         self._server = server
@@ -168,6 +169,10 @@ class XfroutSession():
         self.ClientClass = client_class # parameterize this for testing
         self.ClientClass = client_class # parameterize this for testing
         self._soa = None # will be set in _xfrout_setup or in tests
         self._soa = None # will be set in _xfrout_setup or in tests
         self._jnl_reader = None # will be set to a reader for IXFR
         self._jnl_reader = None # will be set to a reader for IXFR
+        # Set counter handlers for counting Xfr requests. An argument
+        # is required for zone name.
+        self._counter_xfrrej = counter_xfrrej
+        self._counter_xfrreqdone = counter_xfrreqdone
         self._handle()
         self._handle()
 
 
     def create_tsig_ctx(self, tsig_record, tsig_key_ring):
     def create_tsig_ctx(self, tsig_record, tsig_key_ring):
@@ -270,6 +275,9 @@ class XfroutSession():
                          format_zone_str(zone_name, zone_class))
                          format_zone_str(zone_name, zone_class))
             return None, None
             return None, None
         elif acl_result == REJECT:
         elif acl_result == REJECT:
+            if self._counter_xfrrej is not None:
+                # count rejected Xfr request by each zone name
+                self._counter_xfrrej(zone_name.to_text())
             logger.debug(DBG_XFROUT_TRACE, XFROUT_QUERY_REJECTED,
             logger.debug(DBG_XFROUT_TRACE, XFROUT_QUERY_REJECTED,
                          self._request_type, format_addrinfo(self._remote),
                          self._request_type, format_addrinfo(self._remote),
                          format_zone_str(zone_name, zone_class))
                          format_zone_str(zone_name, zone_class))
@@ -525,6 +533,9 @@ class XfroutSession():
         except Exception as err:
         except Exception as err:
             logger.error(XFROUT_XFR_TRANSFER_ERROR, self._request_typestr,
             logger.error(XFROUT_XFR_TRANSFER_ERROR, self._request_typestr,
                     format_addrinfo(self._remote), zone_str, err)
                     format_addrinfo(self._remote), zone_str, err)
+        if self._counter_xfrreqdone is not None:
+            # count done Xfr requests by each zone name
+            self._counter_xfrreqdone(zone_name.to_text())
         logger.info(XFROUT_XFR_TRANSFER_DONE, self._request_typestr,
         logger.info(XFROUT_XFR_TRANSFER_DONE, self._request_typestr,
                     format_addrinfo(self._remote), zone_str)
                     format_addrinfo(self._remote), zone_str)
 
 
@@ -634,7 +645,7 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
     '''The unix domain socket server which accept xfr query sent from auth server.'''
     '''The unix domain socket server which accept xfr query sent from auth server.'''
 
 
     def __init__(self, sock_file, handle_class, shutdown_event, config_data,
     def __init__(self, sock_file, handle_class, shutdown_event, config_data,
-                 cc):
+                 cc, **counters):
         self._remove_unused_sock_file(sock_file)
         self._remove_unused_sock_file(sock_file)
         self._sock_file = sock_file
         self._sock_file = sock_file
         socketserver_mixin.NoPollMixIn.__init__(self)
         socketserver_mixin.NoPollMixIn.__init__(self)
@@ -644,6 +655,8 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
         self._common_init()
         self._common_init()
         self._cc = cc
         self._cc = cc
         self.update_config_data(config_data)
         self.update_config_data(config_data)
+        # handlers for statistics use
+        self._counters = counters
 
 
     def _common_init(self):
     def _common_init(self):
         '''Initialization shared with the mock server class used for tests'''
         '''Initialization shared with the mock server class used for tests'''
@@ -798,7 +811,8 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
         self._lock.release()
         self._lock.release()
         self.RequestHandlerClass(sock_fd, request_data, self,
         self.RequestHandlerClass(sock_fd, request_data, self,
                                  isc.server_common.tsig_keyring.get_keyring(),
                                  isc.server_common.tsig_keyring.get_keyring(),
-                                 self._guess_remote(sock_fd), acl, zone_config)
+                                 self._guess_remote(sock_fd), acl, zone_config,
+                                 **self._counters)
 
 
     def _remove_unused_sock_file(self, sock_file):
     def _remove_unused_sock_file(self, sock_file):
         '''Try to remove the socket file. If the file is being used
         '''Try to remove the socket file. If the file is being used
@@ -926,6 +940,107 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
         self._transfers_counter -= 1
         self._transfers_counter -= 1
         self._lock.release()
         self._lock.release()
 
 
+class XfroutCounter:
+    """A class for handling all statistics counters of Xfrout.  In
+    this class, the structure of per-zone counters is assumed to be
+    like this:
+        zones/example.com./notifyoutv4
+        zones/example.com./notifyoutv6
+        zones/example.com./xfrrej
+        zones/example.com./xfrreqdone
+    """
+    # '_SERVER_' is a special zone name representing an entire
+    # count. It doesn't mean a specific zone, but it means an
+    # entire count in the server.
+    entire_server = '_SERVER_'
+    # zone names are contained under this dirname in the spec file.
+    perzone_prefix = 'zones'
+    def __init__(self, statistics_spec):
+        self._statistics_spec = statistics_spec
+        # holding statistics data for Xfrout module
+        self._statistics_data = {}
+        self._lock = threading.RLock()
+        self._create_perzone_incrementers()
+
+    def get_statistics(self):
+        """Calculates an entire server counts, and returns statistics
+        data format to send out the stats module including each
+        counter. If there is no counts, then it returns an empty
+        dictionary. Locks the thread because it is considered to be
+        invoked by a multi-threading caller."""
+        # If self._statistics_data contains nothing of zone name, it
+        # returns an empty dict.
+        if len(self._statistics_data) == 0: return {}
+        zones = {}
+        with self._lock:
+            zones = self._statistics_data[self.perzone_prefix].copy()
+        # Start calculation for '_SERVER_' counts
+        attrs = self._get_default_statistics_data()[self.perzone_prefix][self.entire_server]
+        statistics_data = {self.perzone_prefix: {}}
+        for attr in attrs:
+            sum_ = 0
+            for name in zones:
+                if name == self.entire_server: continue
+                if attr in zones[name]:
+                    if  name not in statistics_data[self.perzone_prefix]:
+                        statistics_data[self.perzone_prefix][name] = {}
+                    statistics_data[self.perzone_prefix][name].update(
+                        {attr: zones[name][attr]}
+                        )
+                    sum_ += zones[name][attr]
+            if  sum_ > 0:
+                if self.entire_server not in statistics_data[self.perzone_prefix]:
+                    statistics_data[self.perzone_prefix][self.entire_server] = {}
+                statistics_data[self.perzone_prefix][self.entire_server].update({attr: sum_})
+        return statistics_data
+
+    def _get_default_statistics_data(self):
+        """Returns default statistics data from the spec file"""
+        statistics_data = {}
+        for id_ in isc.config.spec_name_list(self._statistics_spec):
+            spec = isc.config.find_spec_part(self._statistics_spec, id_)
+            statistics_data.update({id_: spec['item_default']})
+        return statistics_data
+
+    def _create_perzone_incrementers(self):
+        """Creates increment method of each per-zone counter based on
+        the spec file. Incrementer can be accessed by name
+        "inc_${item_name}".Incrementers are passed to the
+        XfroutSession and NotifyOut class as counter handlers."""
+        # add a new element under the named_set item for the zone
+        zones_spec = isc.config.find_spec_part(
+            self._statistics_spec, self.perzone_prefix)
+        item_list =  isc.config.spec_name_list(\
+            zones_spec['named_set_item_spec']['map_item_spec'])
+        # can be accessed by the name 'inc_xxx'
+        for item in item_list:
+            def __perzone_incrementer(zone_name, counter_name=item, step=1):
+                """A per-zone incrementer for counter_name. Locks the thread
+                because it is considered to be invoked by a multi-threading
+                caller."""
+                with self._lock:
+                    self._add_perzone_counter(zone_name)
+                    self._statistics_data[self.perzone_prefix][zone_name][counter_name] += step
+            setattr(self, 'inc_%s' % item, __perzone_incrementer)
+
+
+    def _add_perzone_counter(self, zone):
+        """Adds named_set-type counter for each zone name"""
+        try:
+            self._statistics_data[self.perzone_prefix][zone]
+        except KeyError:
+            # add a new element under the named_set item for the zone
+            map_spec = isc.config.find_spec_part(
+                self._statistics_spec, '%s/%s' % \
+                    (self.perzone_prefix, zone))['map_item_spec']
+            id_list =  isc.config.spec_name_list(map_spec)
+            for id_ in id_list:
+                spec = isc.config.find_spec_part(map_spec, id_)
+                isc.cc.data.set(self._statistics_data,
+                                '%s/%s/%s' % \
+                                    (self.perzone_prefix, zone, id_),
+                                spec['item_default'])
+
 class XfroutServer:
 class XfroutServer:
     def __init__(self):
     def __init__(self):
         self._unix_socket_server = None
         self._unix_socket_server = None
@@ -933,6 +1048,8 @@ class XfroutServer:
         self._shutdown_event = threading.Event()
         self._shutdown_event = threading.Event()
         self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler)
         self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler)
         self._config_data = self._cc.get_full_config()
         self._config_data = self._cc.get_full_config()
+        self._counter = XfroutCounter(
+            self._cc.get_module_spec().get_statistics_spec())
         self._cc.start()
         self._cc.start()
         self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
         self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
         isc.server_common.tsig_keyring.init_keyring(self._cc)
         isc.server_common.tsig_keyring.init_keyring(self._cc)
@@ -941,17 +1058,25 @@ class XfroutServer:
 
 
     def _start_xfr_query_listener(self):
     def _start_xfr_query_listener(self):
         '''Start a new thread to accept xfr query. '''
         '''Start a new thread to accept xfr query. '''
-        self._unix_socket_server = UnixSockServer(self._listen_sock_file,
-                                                  XfroutSession,
-                                                  self._shutdown_event,
-                                                  self._config_data,
-                                                  self._cc)
+        self._unix_socket_server = UnixSockServer(
+            self._listen_sock_file,
+            XfroutSession,
+            self._shutdown_event,
+            self._config_data,
+            self._cc,
+            counter_xfrrej=self._counter.inc_xfrrej,
+            counter_xfrreqdone=self._counter.inc_xfrreqdone
+            )
         listener = threading.Thread(target=self._unix_socket_server.serve_forever)
         listener = threading.Thread(target=self._unix_socket_server.serve_forever)
         listener.start()
         listener.start()
 
 
     def _start_notifier(self):
     def _start_notifier(self):
         datasrc = self._unix_socket_server.get_db_file()
         datasrc = self._unix_socket_server.get_db_file()
-        self._notifier = notify_out.NotifyOut(datasrc)
+        self._notifier = notify_out.NotifyOut(
+            datasrc,
+            counter_notifyoutv4=self._counter.inc_notifyoutv4,
+            counter_notifyoutv6=self._counter.inc_notifyoutv6
+            )
         if 'also_notify' in self._config_data:
         if 'also_notify' in self._config_data:
             for slave in self._config_data['also_notify']:
             for slave in self._config_data['also_notify']:
                 self._notifier.add_slave(slave['address'], slave['port'])
                 self._notifier.add_slave(slave['address'], slave['port'])
@@ -1027,6 +1152,15 @@ class XfroutServer:
             else:
             else:
                 answer = create_answer(1, "Bad command parameter:" + str(args))
                 answer = create_answer(1, "Bad command parameter:" + str(args))
 
 
+        # return statistics data to the stats daemon
+        elif cmd == "getstats":
+            # The log level is here set to debug in order to avoid
+            # that a log becomes too verbose. Because the b10-stats
+            # daemon is periodically asking to the b10-xfrout daemon.
+            logger.debug(DBG_XFROUT_TRACE, \
+                             XFROUT_RECEIVED_GETSTATS_COMMAND)
+            answer = create_answer(0, self._counter.get_statistics())
+
         else:
         else:
             answer = create_answer(1, "Unknown command:" + str(cmd))
             answer = create_answer(1, "Unknown command:" + str(cmd))
 
 

+ 59 - 0
src/bin/xfrout/xfrout.spec.pre.in

@@ -114,6 +114,65 @@
             "item_default": "IN"
             "item_default": "IN"
           } ]
           } ]
         }
         }
+      ],
+      "statistics": [
+        {
+          "item_name": "zones",
+          "item_type": "named_set",
+          "item_optional": false,
+          "item_default": {
+            "_SERVER_" : {
+              "notifyoutv4" : 0,
+              "notifyoutv6" : 0,
+              "xfrrej" : 0,
+              "xfrreqdone" : 0
+            }
+          },
+          "item_title": "Zone names",
+          "item_description": "Zone names for Xfrout statistics",
+          "named_set_item_spec": {
+            "item_name": "zonename",
+            "item_type": "map",
+            "item_optional": false,
+            "item_default": {},
+            "item_title": "Zone name",
+            "item_description": "Zone name for Xfrout statistics",
+            "map_item_spec": [
+              {
+                "item_name": "notifyoutv4",
+                "item_type": "integer",
+                "item_optional": false,
+                "item_default": 0,
+                "item_title": "IPv4 notifies",
+                "item_description": "Number of IPv4 notifies per zone name sent out from Xfrout"
+              },
+              {
+                "item_name": "notifyoutv6",
+                "item_type": "integer",
+                "item_optional": false,
+                "item_default": 0,
+                "item_title": "IPv6 notifies",
+                "item_description": "Number of IPv6 notifies per zone name sent out from Xfrout"
+              },
+              {
+                "item_name": "xfrrej",
+                "item_type": "integer",
+                "item_optional": false,
+                "item_default": 0,
+                "item_title": "XFR rejected requests",
+                "item_description": "Number of XFR requests per zone name rejected by Xfrout"
+              },
+              {
+                "item_name": "xfrreqdone",
+                "item_type": "integer",
+                "item_optional": false,
+                "item_default": 0,
+                "item_title": "Requested zone transfers",
+                "item_description": "Number of requested zone transfers completed per zone name"
+              }
+            ]
+          }
+        }
       ]
       ]
   }
   }
 }
 }

+ 4 - 0
src/bin/xfrout/xfrout_messages.mes

@@ -107,6 +107,10 @@ received from the configuration manager.
 The xfrout daemon received a command on the command channel that
 The xfrout daemon received a command on the command channel that
 NOTIFY packets should be sent for the given zone.
 NOTIFY packets should be sent for the given zone.
 
 
+% XFROUT_RECEIVED_GETSTATS_COMMAND received command to get statistics data
+The xfrout daemon received a command on the command channel that
+statistics data should be sent to the stats daemon.
+
 % XFROUT_PARSE_QUERY_ERROR error parsing query: %1
 % XFROUT_PARSE_QUERY_ERROR error parsing query: %1
 There was a parse error while reading an incoming query. The parse
 There was a parse error while reading an incoming query. The parse
 error is shown in the log message. A remote client sent a packet we
 error is shown in the log message. A remote client sent a packet we

+ 16 - 2
src/lib/python/isc/notify/notify_out.py

@@ -125,9 +125,10 @@ class ZoneNotifyInfo:
 class NotifyOut:
 class NotifyOut:
     '''This class is used to handle notify logic for all zones(sending
     '''This class is used to handle notify logic for all zones(sending
     notify message to its slaves). notify service can be started by
     notify message to its slaves). notify service can be started by
-    calling  dispatcher(), and it can be stoped by calling shutdown()
+    calling  dispatcher(), and it can be stopped by calling shutdown()
     in another thread. '''
     in another thread. '''
-    def __init__(self, datasrc_file, verbose=True):
+    def __init__(self, datasrc_file, counter_handler=None, verbose=True,
+                 counter_notifyoutv4=None, counter_notifyoutv6=None):
         self._notify_infos = {} # key is (zone_name, zone_class)
         self._notify_infos = {} # key is (zone_name, zone_class)
         self._waiting_zones = []
         self._waiting_zones = []
         self._notifying_zones = []
         self._notifying_zones = []
@@ -142,6 +143,10 @@ class NotifyOut:
         # Use nonblock event to eliminate busy loop
         # Use nonblock event to eliminate busy loop
         # If there are no notifying zones, clear the event bit and wait.
         # If there are no notifying zones, clear the event bit and wait.
         self._nonblock_event = threading.Event()
         self._nonblock_event = threading.Event()
+        # Set counter handlers for counting notifies. An argument is
+        # required for zone name.
+        self._counter_notifyoutv4 = counter_notifyoutv4
+        self._counter_notifyoutv6 = counter_notifyoutv6
 
 
     def _init_notify_out(self, datasrc_file):
     def _init_notify_out(self, datasrc_file):
         '''Get all the zones name and its notify target's address.
         '''Get all the zones name and its notify target's address.
@@ -478,6 +483,15 @@ class NotifyOut:
         try:
         try:
             sock = zone_notify_info.create_socket(addrinfo[0])
             sock = zone_notify_info.create_socket(addrinfo[0])
             sock.sendto(render.get_data(), 0, addrinfo)
             sock.sendto(render.get_data(), 0, addrinfo)
+            # count notifying by IPv4 or IPv6 for statistics
+            if zone_notify_info.get_socket().family \
+                    == socket.AF_INET \
+                    and self._counter_notifyoutv4 is not None:
+                self._counter_notifyoutv4(zone_notify_info.zone_name)
+            elif zone_notify_info.get_socket().family \
+                    == socket.AF_INET6 \
+                    and self._counter_notifyoutv6 is not None:
+                self._counter_notifyoutv6(zone_notify_info.zone_name)
             logger.info(NOTIFY_OUT_SENDING_NOTIFY, addrinfo[0],
             logger.info(NOTIFY_OUT_SENDING_NOTIFY, addrinfo[0],
                         addrinfo[1])
                         addrinfo[1])
         except (socket.error, addr.InvalidAddress) as err:
         except (socket.error, addr.InvalidAddress) as err:

+ 56 - 1
src/lib/python/isc/notify/tests/notify_out_test.py

@@ -61,6 +61,7 @@ class MockZoneNotifyInfo(notify_out.ZoneNotifyInfo):
         self.sock_family = self._sock.family
         self.sock_family = self._sock.family
         self._sock.close()
         self._sock.close()
         self._sock = MockSocket()
         self._sock = MockSocket()
+        self._sock.family = self.sock_family
         return self._sock
         return self._sock
 
 
 class TestZoneNotifyInfo(unittest.TestCase):
 class TestZoneNotifyInfo(unittest.TestCase):
@@ -95,7 +96,13 @@ class TestZoneNotifyInfo(unittest.TestCase):
 class TestNotifyOut(unittest.TestCase):
 class TestNotifyOut(unittest.TestCase):
     def setUp(self):
     def setUp(self):
         self._db_file = TESTDATA_SRCDIR + '/test.sqlite3'
         self._db_file = TESTDATA_SRCDIR + '/test.sqlite3'
-        self._notify = notify_out.NotifyOut(self._db_file)
+        self._notifiedv4_zone_name = None
+        def _dummy_counter_notifyoutv4(z): self._notifiedv4_zone_name = z
+        self._notifiedv6_zone_name = None
+        def _dummy_counter_notifyoutv6(z): self._notifiedv6_zone_name = z
+        self._notify = notify_out.NotifyOut(self._db_file,
+                                            counter_notifyoutv4=_dummy_counter_notifyoutv4,
+                                            counter_notifyoutv6=_dummy_counter_notifyoutv6)
         self._notify._notify_infos[('example.com.', 'IN')] = MockZoneNotifyInfo('example.com.', 'IN')
         self._notify._notify_infos[('example.com.', 'IN')] = MockZoneNotifyInfo('example.com.', 'IN')
         self._notify._notify_infos[('example.com.', 'CH')] = MockZoneNotifyInfo('example.com.', 'CH')
         self._notify._notify_infos[('example.com.', 'CH')] = MockZoneNotifyInfo('example.com.', 'CH')
         self._notify._notify_infos[('example.net.', 'IN')] = MockZoneNotifyInfo('example.net.', 'IN')
         self._notify._notify_infos[('example.net.', 'IN')] = MockZoneNotifyInfo('example.net.', 'IN')
@@ -262,17 +269,61 @@ class TestNotifyOut(unittest.TestCase):
     def test_send_notify_message_udp_ipv4(self):
     def test_send_notify_message_udp_ipv4(self):
         example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
         example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
         example_com_info.prepare_notify_out()
         example_com_info.prepare_notify_out()
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
         ret = self._notify._send_notify_message_udp(example_com_info,
         ret = self._notify._send_notify_message_udp(example_com_info,
                                                     ('192.0.2.1', 53))
                                                     ('192.0.2.1', 53))
         self.assertTrue(ret)
         self.assertTrue(ret)
         self.assertEqual(socket.AF_INET, example_com_info.sock_family)
         self.assertEqual(socket.AF_INET, example_com_info.sock_family)
+        self.assertEqual(self._notifiedv4_zone_name, 'example.net.')
+        self.assertIsNone(self._notifiedv6_zone_name)
 
 
     def test_send_notify_message_udp_ipv6(self):
     def test_send_notify_message_udp_ipv6(self):
         example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
         example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
         ret = self._notify._send_notify_message_udp(example_com_info,
         ret = self._notify._send_notify_message_udp(example_com_info,
                                                     ('2001:db8::53', 53))
                                                     ('2001:db8::53', 53))
         self.assertTrue(ret)
         self.assertTrue(ret)
         self.assertEqual(socket.AF_INET6, example_com_info.sock_family)
         self.assertEqual(socket.AF_INET6, example_com_info.sock_family)
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertEqual(self._notifiedv6_zone_name, 'example.net.')
+
+    def test_send_notify_message_udp_ipv4_with_nonetype_notifyoutv4(self):
+        example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
+        example_com_info.prepare_notify_out()
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
+        self._notify._counter_notifyoutv4 = None
+        self._notify._send_notify_message_udp(example_com_info,
+                                              ('192.0.2.1', 53))
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
+
+    def test_send_notify_message_udp_ipv4_with_notcallable_notifyoutv4(self):
+        example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
+        example_com_info.prepare_notify_out()
+        self._notify._counter_notifyoutv4 = 'NOT CALLABLE'
+        self.assertRaises(TypeError,
+                          self._notify._send_notify_message_udp,
+                          example_com_info, ('192.0.2.1', 53))
+
+    def test_send_notify_message_udp_ipv6_with_nonetype_notifyoutv6(self):
+        example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
+        self._notify._counter_notifyoutv6 = None
+        self._notify._send_notify_message_udp(example_com_info,
+                                              ('2001:db8::53', 53))
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
+
+    def test_send_notify_message_udp_ipv6_with_notcallable_notifyoutv6(self):
+        example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
+        self._notify._counter_notifyoutv6 = 'NOT CALLABLE'
+        self.assertRaises(TypeError,
+                          self._notify._send_notify_message_udp,
+                          example_com_info, ('2001:db8::53', 53))
 
 
     def test_send_notify_message_with_bogus_address(self):
     def test_send_notify_message_with_bogus_address(self):
         example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
         example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
@@ -281,9 +332,13 @@ class TestNotifyOut(unittest.TestCase):
         # happen, but right now it's not actually the case.  Even if the
         # happen, but right now it's not actually the case.  Even if the
         # data source does its job, it's prudent to confirm the behavior for
         # data source does its job, it's prudent to confirm the behavior for
         # an unexpected case.
         # an unexpected case.
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
         ret = self._notify._send_notify_message_udp(example_com_info,
         ret = self._notify._send_notify_message_udp(example_com_info,
                                                     ('invalid', 53))
                                                     ('invalid', 53))
         self.assertFalse(ret)
         self.assertFalse(ret)
+        self.assertIsNone(self._notifiedv4_zone_name)
+        self.assertIsNone(self._notifiedv6_zone_name)
 
 
     def test_zone_notify_handler(self):
     def test_zone_notify_handler(self):
         old_send_msg = self._notify._send_notify_message_udp
         old_send_msg = self._notify._send_notify_message_udp

+ 1 - 0
tests/lettuce/configurations/xfrin/.gitignore

@@ -0,0 +1 @@
+/retransfer_master.conf

+ 1 - 0
tests/lettuce/configurations/xfrin/retransfer_master.conf

@@ -38,6 +38,7 @@
             "b10-auth": { "kind": "needed", "special": "auth" },
             "b10-auth": { "kind": "needed", "special": "auth" },
             "b10-xfrout": { "address": "Xfrout", "kind": "dispensable" },
             "b10-xfrout": { "address": "Xfrout", "kind": "dispensable" },
             "b10-zonemgr": { "address": "Zonemgr", "kind": "dispensable" },
             "b10-zonemgr": { "address": "Zonemgr", "kind": "dispensable" },
+            "b10-stats": { "address": "Stats", "kind": "dispensable" },
             "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
             "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
         }
         }
     }
     }

+ 61 - 0
tests/lettuce/features/terrain/bind10_control.py

@@ -362,3 +362,64 @@ def configure_ddns_off(step):
         config commit
         config commit
         \"\"\"
         \"\"\"
     """)
     """)
+
+@step('query statistics(?: (\S+))? of bind10 module (\S+)(?: with cmdctl port (\d+))?')
+def query_statistics(step, statistics, name, cmdctl_port):
+    """
+    query statistics data via bindctl.
+    Parameters:
+    statistics  ('statistics <statistics>', optional) : The queried statistics name.
+    name ('module <name>'): The name of the module (case sensitive!)
+    cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
+                the command to.
+    """
+    port_str = ' with cmdctl port %s' % cmdctl_port \
+        if cmdctl_port else ''
+    step.given('send bind10%s the command Stats show owner=%s%s'\
+        % (port_str, name,\
+               ' name=%s' % statistics if statistics else ''))
+
+def find_value(dictionary, key):
+    """A helper method. Recursively find a value corresponding to the
+    key of the dictionary and returns it. Returns None if the
+    dictionary is not dict type."""
+    if type(dictionary) is not dict:
+        return
+    if key in dictionary:
+        return dictionary[key]
+    else:
+        for v in dictionary.values():
+            return find_value(v, key)
+
+@step('The counter (\S+)(?: for the zone (\S+))? should be' + \
+          '(?:( greater than| less than))? (\d+)')
+def check_statistics(step, counter, zone, gtlt, number):
+    """
+    check the output of bindctl for statistics of specified counter
+    and zone.
+    Parameters:
+    counter ('counter <counter>'): The counter name of statistics.
+    zone ('zone <zone>', optional): The zone name.
+    gtlt (' greater than'|' less than', optional): greater than
+          <number> or less than <number>.
+    number ('<number>): The expect counter number. <number> is assumed
+          to be an unsigned integer.
+    """
+    output = parse_bindctl_output_as_data_structure()
+    found = None
+    zone_str = ""
+    if zone:
+        found = find_value(find_value(output, zone), counter)
+        zone_str = " for zone %s" % zone
+    else:
+        found = find_value(output, counter)
+    assert found is not None, \
+        'Not found statistics counter %s%s' % (counter, zone_str)
+    msg = "Got %s, expected%s %s as counter %s%s" % \
+        (found, gtlt, number, counter, zone_str)
+    if gtlt and 'greater' in gtlt:
+        assert int(found) > int(number), msg
+    elif gtlt and 'less' in gtlt:
+        assert int(found) < int(number), msg
+    else:
+        assert int(found) == int(number), msg

+ 2 - 0
tests/lettuce/features/terrain/terrain.py

@@ -57,6 +57,8 @@ copylist = [
      "configurations/ddns/ddns.config"],
      "configurations/ddns/ddns.config"],
     ["configurations/ddns/noddns.config.orig",
     ["configurations/ddns/noddns.config.orig",
      "configurations/ddns/noddns.config"],
      "configurations/ddns/noddns.config"],
+    ["configurations/xfrin/retransfer_master.conf.orig",
+     "configurations/xfrin/retransfer_master.conf"],
     ["data/inmem-xfrin.sqlite3.orig",
     ["data/inmem-xfrin.sqlite3.orig",
      "data/inmem-xfrin.sqlite3"],
      "data/inmem-xfrin.sqlite3"],
     ["data/xfrin-notify.sqlite3.orig",
     ["data/xfrin-notify.sqlite3.orig",

+ 110 - 0
tests/lettuce/features/xfrin_notify_handling.feature

@@ -8,6 +8,7 @@ Feature: Xfrin incoming notify handling
     And wait for master stderr message AUTH_SERVER_STARTED
     And wait for master stderr message AUTH_SERVER_STARTED
     And wait for master stderr message XFROUT_STARTED
     And wait for master stderr message XFROUT_STARTED
     And wait for master stderr message ZONEMGR_STARTED
     And wait for master stderr message ZONEMGR_STARTED
+    And wait for master stderr message STATS_STARTING
 
 
     And I have bind10 running with configuration xfrin/retransfer_slave_notify.conf
     And I have bind10 running with configuration xfrin/retransfer_slave_notify.conf
     And wait for bind10 stderr message BIND10_STARTED_CC
     And wait for bind10 stderr message BIND10_STARTED_CC
@@ -18,6 +19,19 @@ Feature: Xfrin incoming notify handling
 
 
     A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
     A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
 
 
+    #
+    # Test for statistics
+    #
+    # check for initial statistics
+    #
+    When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
+    last bindctl output should not contain "error"
+    last bindctl output should not contain "example.org."
+    The counter notifyoutv4 for the zone _SERVER_ should be 0
+    The counter notifyoutv6 for the zone _SERVER_ should be 0
+    The counter xfrrej for the zone _SERVER_ should be 0
+    The counter xfrreqdone for the zone _SERVER_ should be 0
+
     When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
     When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
     Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
     Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
     Then wait for new bind10 stderr message AUTH_RECEIVED_NOTIFY
     Then wait for new bind10 stderr message AUTH_RECEIVED_NOTIFY
@@ -25,5 +39,101 @@ Feature: Xfrin incoming notify handling
     Then wait for new bind10 stderr message XFRIN_XFR_TRANSFER_STARTED
     Then wait for new bind10 stderr message XFRIN_XFR_TRANSFER_STARTED
     Then wait for new bind10 stderr message XFRIN_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
     Then wait for new bind10 stderr message XFRIN_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
     Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
     Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
+    Then wait 5 times for new master stderr message NOTIFY_OUT_SENDING_NOTIFY
+    Then wait for new master stderr message NOTIFY_OUT_RETRY_EXCEEDED
 
 
     A query for www.example.org to [::1]:47806 should have rcode NOERROR
     A query for www.example.org to [::1]:47806 should have rcode NOERROR
+
+    #
+    # Test for statistics
+    #
+    # check for statistics change
+    #
+    When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
+    last bindctl output should not contain "error"
+    Then wait for new master stderr message XFROUT_RECEIVED_GETSTATS_COMMAND
+    The counter notifyoutv4 for the zone _SERVER_ should be 0
+    The counter notifyoutv4 for the zone example.org. should be 0
+    The counter notifyoutv6 for the zone _SERVER_ should be 5
+    The counter notifyoutv6 for the zone example.org. should be 5
+    The counter xfrrej for the zone _SERVER_ should be 0
+    The counter xfrrej for the zone example.org. should be 0
+    The counter xfrreqdone for the zone _SERVER_ should be 1
+    The counter xfrreqdone for the zone example.org. should be 1
+
+    #
+    # Test for Xfr request rejected
+    #
+    Scenario: Handle incoming notify (XFR request rejected)
+    Given I have bind10 running with configuration xfrin/retransfer_master.conf with cmdctl port 47804 as master
+    And wait for master stderr message BIND10_STARTED_CC
+    And wait for master stderr message CMDCTL_STARTED
+    And wait for master stderr message AUTH_SERVER_STARTED
+    And wait for master stderr message XFROUT_STARTED
+    And wait for master stderr message ZONEMGR_STARTED
+    And wait for master stderr message STATS_STARTING
+
+    And I have bind10 running with configuration xfrin/retransfer_slave_notify.conf
+    And wait for bind10 stderr message BIND10_STARTED_CC
+    And wait for bind10 stderr message CMDCTL_STARTED
+    And wait for bind10 stderr message AUTH_SERVER_STARTED
+    And wait for bind10 stderr message XFRIN_STARTED
+    And wait for bind10 stderr message ZONEMGR_STARTED
+
+    A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
+
+    #
+    # Test1 for statistics
+    #
+    # check for initial statistics
+    #
+    When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
+    last bindctl output should not contain "error"
+    last bindctl output should not contain "example.org."
+    The counter notifyoutv4 for the zone _SERVER_ should be 0
+    The counter notifyoutv6 for the zone _SERVER_ should be 0
+    The counter xfrrej for the zone _SERVER_ should be 0
+    The counter xfrreqdone for the zone _SERVER_ should be 0
+
+    #
+    # set transfer_acl rejection
+    # Local xfr requests from Xfrin module would be rejected here.
+    #
+    When I send bind10 the following commands with cmdctl port 47804
+    """
+    config set Xfrout/zone_config[0]/transfer_acl [{"action":  "REJECT", "from": "::1"}]
+    config commit
+    """
+    last bindctl output should not contain Error
+
+    When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
+    Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
+    Then wait for new bind10 stderr message AUTH_RECEIVED_NOTIFY
+    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_NOTIFY
+    Then wait for new bind10 stderr message XFRIN_XFR_TRANSFER_STARTED
+    Then wait for new bind10 stderr message XFRIN_XFR_TRANSFER_PROTOCOL_ERROR not XFRIN_XFR_TRANSFER_STARTED
+    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED not ZONEMGR_RECEIVE_XFRIN_SUCCESS
+    Then wait 5 times for new master stderr message NOTIFY_OUT_SENDING_NOTIFY
+    Then wait for new master stderr message NOTIFY_OUT_RETRY_EXCEEDED
+
+    A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
+
+    #
+    # Test2 for statistics
+    #
+    # check for statistics change
+    #
+    When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
+    last bindctl output should not contain "error"
+    The counter notifyoutv4 for the zone _SERVER_ should be 0
+    The counter notifyoutv4 for the zone example.org. should be 0
+    The counter notifyoutv6 for the zone _SERVER_ should be 5
+    The counter notifyoutv6 for the zone example.org. should be 5
+    # The counts of rejection would be between 1 and 2. They are not
+    # fixed. It would depend on timing or the platform.
+    The counter xfrrej for the zone _SERVER_ should be greater than 0
+    The counter xfrrej for the zone _SERVER_ should be less than 3
+    The counter xfrrej for the zone example.org. should be greater than 0
+    The counter xfrrej for the zone example.org. should be less than 3
+    The counter xfrreqdone for the zone _SERVER_ should be 0
+    The counter xfrreqdone for the zone example.org. should be 0