Browse Source

[2934] added a lettuce test for axfr, including the scenario of this ticket.

JINMEI Tatuya 12 years ago
parent
commit
2ab76e6181

+ 41 - 0
tests/lettuce/configurations/xfrout_master.conf

@@ -0,0 +1,41 @@
+{
+    "version": 3,
+    "Logging": {
+        "loggers": [ {
+            "debuglevel": 99,
+            "severity": "DEBUG",
+            "name": "*"
+        } ]
+    },
+    "Auth": {
+        "database_file": "data/xfrout.sqlite3",
+        "listen_on": [ {
+            "address": "::1",
+            "port": 47806
+        } ]
+    },
+    "data_sources": {
+        "classes": {
+            "IN": [{
+                "type": "sqlite3",
+                "params": {
+                    "database_file": "data/xfrout.sqlite3"
+                }
+            }]
+        }
+    },
+    "Xfrout": {
+        "zone_config": [ {
+            "origin": "example.org"
+        } ]
+    },
+    "Init": {
+        "components": {
+            "b10-auth": { "kind": "needed", "special": "auth" },
+            "b10-xfrout": { "address": "Xfrout", "kind": "dispensable" },
+            "b10-zonemgr": { "address": "Zonemgr", "kind": "dispensable" },
+            "b10-stats": { "address": "Stats", "kind": "dispensable" },
+            "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
+        }
+    }
+}

+ 86 - 0
tests/lettuce/features/terrain/loadzone.py

@@ -0,0 +1,86 @@
+# Copyright (C) 2013  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 lettuce import *
+import subprocess
+import tempfile
+
+def run_loadzone(zone, zone_file, db_file):
+    """Run b10-loadzone.
+
+    It currently only works for an SQLite3-based data source, and takes
+    its DB file as a parameter.
+
+    Parameters:
+    zone (str): the zone name
+    zone_file (str): master zone file for the zone
+    db_file (str): SQLite3 DB file to load the zone into
+
+    """
+    sqlite_datasrc_cfg = '{"database_file": "' + db_file + '"}'
+    args = ['b10-loadzone', '-c', sqlite_datasrc_cfg, zone, zone_file]
+    loadzone = subprocess.Popen(args, 1, None, None,
+                                subprocess.PIPE, subprocess.PIPE)
+    (stdout, stderr) = loadzone.communicate()
+    result = loadzone.returncode
+    world.last_loadzone_stdout = stdout
+    world.last_loadzone_stderr = stderr
+    assert result == 0, "loadzone exit code: " + str(result) +\
+                        "\nstdout:\n" + str(stdout) +\
+                        "stderr:\n" + str(stderr)
+
+@step('load zone (\S+) to DB file (\S+) from (\S+)')
+def load_zone_to_dbfile(step, zone, db_file, zone_file):
+    """Load a zone into a data source from a zone file.
+
+    It currently only works for an SQLite3-based data source.  Its
+    DB file name should be specified.
+
+    Step definition:
+    load zone <zone_name> to DB file <db_file> from <zone_file>
+
+    """
+    run_loadzone(zone, zone_file, db_file)
+
+@step('load (\d+) records for zone (\S+) to DB file (\S+)')
+def load_zone_rr_to_dbfile(step, num_records, zone, db_file):
+    """Load a zone with a specified number of RRs into a data source.
+
+    It currently only works for an SQLite3-based data source.  Its
+    DB file name should be specified.
+
+    It creates the content of the zone dynamically so the total number of
+    RRs of the zone is the specified number, including mandatory SOA and NS.
+
+    Step definition:
+    load zone <zone_name> to DB file <db_file> from <zone_file>
+
+    """
+    num_records = int(num_records)
+    assert num_records >= 2, 'zone must have at least 2 RRs: SOA and NS'
+    num_records -= 2
+    with tempfile.NamedTemporaryFile(mode='w', prefix='zone-file',
+                                     dir='data/', delete=True) as f:
+        filename = f.name
+        f.write('$TTL 3600\n')
+        f.write('$ORIGIN .\n')  # so it'll work whether or not zone ends with .
+        f.write(zone + ' IN SOA . . 0 0 0 0 0\n')
+        f.write(zone + ' IN NS 0.' + zone + '\n')
+        count = 0
+        while count < num_records:
+            f.write(str(count) + '.' + zone + ' IN A 192.0.2.1\n')
+            count += 1
+        f.flush()
+        run_loadzone(zone, f.name, db_file)

+ 3 - 1
tests/lettuce/features/terrain/terrain.py

@@ -83,7 +83,9 @@ copylist = [
     ["data/xfrin-notify.sqlite3.orig",
      "data/xfrin-notify.sqlite3"],
     ["data/ddns/example.org.sqlite3.orig",
-     "data/ddns/example.org.sqlite3"]
+     "data/ddns/example.org.sqlite3"],
+    ["data/empty_db.sqlite3",
+     "data/xfrout.sqlite3"]
 ]
 
 # This is a list of files that, if present, will be removed before a scenario

+ 63 - 8
tests/lettuce/features/terrain/transfer.py

@@ -18,7 +18,8 @@
 # and inspect the results.
 #
 # Like querying.py, it uses dig to do the transfers, and
-# places its output in a result structure
+# places its output in a result structure.  It also uses a custom client
+# implementation for less normal operations.
 #
 # This is done in a different file with different steps than
 # querying, because the format of dig's output is
@@ -58,7 +59,16 @@ class TransferResult(object):
             if len(line) > 0 and line[0] != ';':
                 self.records.append(line)
 
-@step('An AXFR transfer of ([\w.]+)(?: from ([^:]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?')
+def parse_addr_port(address, port):
+    if address is None:
+        address = "::1"         # default address
+    # convert [IPv6_addr] to IPv6_addr:
+    address = re.sub(r"\[(.+)\]", r"\1", address)
+    if port is None:
+        port = 47806            # default port
+    return (address, port)
+
+@step('An AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?')
 def perform_axfr(step, zone_name, address, port):
     """
     Perform an AXFR transfer, and store the result as an instance of
@@ -70,15 +80,60 @@ def perform_axfr(step, zone_name, address, port):
     Address defaults to ::1
     Port defaults to 47806
     """
-    if address is None:
-        address = "::1"
-    # convert [IPv6_addr] to IPv6_addr:
-    address = re.sub(r"\[(.+)\]", r"\1", address)
-    if port is None:
-        port = 47806
+    (address, port) = parse_addr_port(address, port)
     args = [ 'dig', 'AXFR', '@' + str(address), '-p', str(port), zone_name ]
     world.transfer_result = TransferResult(args)
 
+@step('A customized AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?(?: with pose of (\d+) second)?')
+def perform_custom_axfr(step, zone_name, address, port, delay):
+    """Checks AXFR transfer, and store the result in the form of internal
+    CustomTransferResult class, which is compatible with TransferResult.
+
+    Step definition:
+    A customized AXFR transfer of <zone_name> [from <address>:<port>] [with pose of <delay> second]
+
+    If optional delay is specified (not None), it waits for the specified
+    seconds after sending the AXFR query before starting receiving
+    responses.  This emulates a slower AXFR client.
+
+    """
+
+    class CustomTransferResult:
+        """Store transfer result only on the number of received answer RRs.
+
+        To be compatible with TransferResult it stores the result in the
+        'records' attribute, which is a list.  But its content is
+        meaningless; its only use is to be used with
+        check_transfer_result_count where its length is of concern.
+
+        """
+        def __init__(self):
+            self.records = []
+
+    # Build arguments and run xfr-client.py.  On success, it simply dumps
+    # the number of received answer RRs to stdout.
+    (address, port) = parse_addr_port(address, port)
+    args = ['/bin/sh', 'run_python-tool.sh', 'tools/xfr-client.py',
+            '-s', address, '-p', str(port)]
+    if delay is not None:
+        args.extend(['-d', delay])
+    args.append(zone_name)
+    client = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
+                              subprocess.PIPE)
+    (stdout, stderr) = client.communicate()
+    result = client.returncode
+    world.last_client_stdout = stdout
+    world.last_client_stderr = stderr
+    assert result == 0, "xfr-client exit code: " + str(result) +\
+                        "\nstdout:\n" + str(stdout) +\
+                        "stderr:\n" + str(stderr)
+    num_rrs = int(stdout.strip())
+
+    # Make the result object, storing dummy value (None) for the number of
+    # answer RRs in the records list.
+    world.transfer_result = CustomTransferResult()
+    world.transfer_result.records = [None for _ in range(0, num_rrs)]
+
 @step('An IXFR transfer of ([\w.]+) (\d+)(?: from ([^:]+)(?::([0-9]+))?)?(?: over (tcp|udp))?')
 def perform_ixfr(step, zone_name, serial, address, port, protocol):
     """

+ 34 - 0
tests/lettuce/features/xfrout_bind10.feature

@@ -0,0 +1,34 @@
+Feature: Xfrout
+    Tests for Xfrout, specific for BIND 10 behaviour.
+
+    Scenario: normal transfer with a moderate number of RRs
+
+    Load 100 records for zone example.org to DB file data/xfrout.sqlite3
+
+    Given I have bind10 running with configuration xfrout_master.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 XFROUT_STARTED
+    And wait for bind10 stderr message ZONEMGR_STARTED
+
+    # The transferred zone should have the generated 100 RRs plush one
+    # trailing SOA.
+    When I do a customized AXFR transfer of example.org
+    Then transfer result should have 101 rrs
+
+    # Similar to the previous one, but using a much larger zone, and with
+    # a small delay at the client side.  It should still succeed.
+    Scenario: transfer a large zone
+
+    Load 50000 records for zone example.org to DB file data/xfrout.sqlite3
+
+    Given I have bind10 running with configuration xfrout_master.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 XFROUT_STARTED
+    And wait for bind10 stderr message ZONEMGR_STARTED
+
+    When I do a customized AXFR transfer of example.org from [::1]:47806 with pose of 5 seconds
+    Then transfer result should have 50001 rrs

+ 23 - 0
tests/lettuce/run_python-tool.sh

@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# Copyright (C) 2013  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# This script runs the specified python program, referring to the in-tree
+# BIND 10 Python libraries (in case the program needs them)
+# usage example: run_python-tool.sh tools/xfr-client.py -p 5300 example.org
+
+. setup_intree_bind10.sh
+$PYTHON_EXEC $*

+ 103 - 0
tests/lettuce/tools/xfr-client.py

@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2013  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.
+
+# A simple XFR client program with some customized behavior.
+# This is intended to provide some less common or even invalid client behavior
+# for some specific tests on outbound zone transfer.
+# It currently only supports AXFR, but can be extended to support IXFR
+# as we see the need for it.
+#
+# For command line usage, run this program with -h option.
+
+from isc.dns import *
+import sys
+import socket
+import struct
+import time
+from optparse import OptionParser
+
+parser = OptionParser(usage='usage: %prog [options] zone_name')
+parser.add_option('-d', '--delay', dest='delay', action='store', default=None,
+                  help='delay (sec) before receiving responses, ' +
+                  'emulating slow clients')
+parser.add_option('-s', '--server', dest='server_addr', action='store',
+                  default='::1',
+                  help="master server's address [default: %default]")
+parser.add_option('-p', '--port', dest='server_port', action='store',
+                  default=53,
+                  help="master server's TCP port [default: %default]")
+(options, args) = parser.parse_args()
+
+if len(args) == 0:
+    parser.error('missing argument')
+
+# Parse arguments and options, and creates client socket.
+zone_name = Name(args[0])
+gai = socket.getaddrinfo(options.server_addr, int(options.server_port), 0,
+                         socket.SOCK_STREAM, socket.IPPROTO_TCP,
+                         socket.AI_NUMERICHOST|socket.AI_NUMERICSERV)
+server_family, server_socktype, server_proto, server_sockaddr = \
+    (gai[0][0], gai[0][1], gai[0][2], gai[0][4])
+s = socket.socket(server_family, server_socktype, server_proto)
+s.connect(server_sockaddr)
+s.settimeout(60)                # safety net in case of hangup situation
+
+# Build XFR query.
+axfr_qry = Message(Message.RENDER)
+axfr_qry.set_rcode(Rcode.NOERROR)
+axfr_qry.set_opcode(Opcode.QUERY)
+axfr_qry.add_question(Question(zone_name, RRClass.IN, RRType.AXFR))
+
+renderer = MessageRenderer()
+axfr_qry.to_wire(renderer)
+qry_data = renderer.get_data()
+
+# Send the query
+hlen_data = struct.pack('H', socket.htons(len(qry_data)))
+s.send(hlen_data)
+s.send(qry_data)
+
+# If specified wait for the given period
+if options.delay is not None:
+    time.sleep(int(options.delay))
+
+def get_request_response(s, size):
+    """A helper function to receive data of specified length from a socket."""
+    recv_size = 0
+    data = b''
+    while recv_size < size:
+        need_recv_size = size - recv_size
+        tmp_data = s.recv(need_recv_size)
+        if len(tmp_data) == 0:
+            return None
+        recv_size += len(tmp_data)
+        data += tmp_data
+    return data
+
+# Receive responses until the connection is terminated, and dump the
+# number of received answer RRs to stdout.
+num_rrs = 0
+while True:
+    hlen_data = get_request_response(s, 2)
+    if hlen_data is None:
+        break
+    resp_data = get_request_response(
+        s, socket.ntohs(struct.unpack('H', hlen_data)[0]))
+    msg = Message(Message.PARSE)
+    msg.from_wire(resp_data, Message.PRESERVE_ORDER)
+    num_rrs += msg.get_rr_count(Message.SECTION_ANSWER)
+print(str(num_rrs))