transfer.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. # Copyright (C) 2011 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. # This script provides transfer (ixfr/axfr) test functionality
  16. # It provides steps to perform the client side of a transfer,
  17. # and inspect the results.
  18. #
  19. # Like querying.py, it uses dig to do the transfers, and
  20. # places its output in a result structure. It also uses a custom client
  21. # implementation for less normal operations.
  22. #
  23. # This is done in a different file with different steps than
  24. # querying, because the format of dig's output is
  25. # very different than that of normal queries
  26. from lettuce import *
  27. import subprocess
  28. import re
  29. class TransferResult(object):
  30. """This object stores transfer results, which is essentially simply
  31. a list of RR strings. These are stored, as read from dig's output,
  32. in the list 'records'. So for an IXFR transfer it contains
  33. the exact result as returned by the server.
  34. If this list is empty, the transfer failed for some reason (dig
  35. does not really show error results well, unfortunately).
  36. We may add some smarter inspection functionality to this class
  37. later.
  38. """
  39. def __init__(self, args):
  40. """Perform the transfer by calling dig, and store the results.
  41. args is the array of arguments to pass to Popen(), this
  42. is passed as is since for IXFR and AXFR there can be very
  43. different options"""
  44. self.records = []
  45. # Technically, using a pipe here can fail; since we don't expect
  46. # large output right now, this works, but should we get a test
  47. # where we do have a lot of output, this could block, and we will
  48. # need to read the output in a different way.
  49. dig_process = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
  50. None)
  51. result = dig_process.wait()
  52. assert result == 0
  53. for l in dig_process.stdout:
  54. line = l.strip()
  55. if len(line) > 0 and line[0] != ';':
  56. self.records.append(line)
  57. def parse_addr_port(address, port):
  58. if address is None:
  59. address = "::1" # default address
  60. # convert [IPv6_addr] to IPv6_addr:
  61. address = re.sub(r"\[(.+)\]", r"\1", address)
  62. if port is None:
  63. port = 47806 # default port
  64. return (address, port)
  65. @step('An AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?')
  66. def perform_axfr(step, zone_name, address, port):
  67. """
  68. Perform an AXFR transfer, and store the result as an instance of
  69. TransferResult in world.transfer_result.
  70. Step definition:
  71. An AXFR transfer of <zone_name> [from <address>:<port>]
  72. Address defaults to ::1
  73. Port defaults to 47806
  74. """
  75. (address, port) = parse_addr_port(address, port)
  76. args = [ 'dig', 'AXFR', '@' + str(address), '-p', str(port), zone_name ]
  77. world.transfer_result = TransferResult(args)
  78. @step('A customized AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?(?: with pose of (\d+) seconds?)?')
  79. def perform_custom_axfr(step, zone_name, address, port, delay):
  80. """Checks AXFR transfer, and store the result in the form of internal
  81. CustomTransferResult class, which is compatible with TransferResult.
  82. Step definition:
  83. A customized AXFR transfer of <zone_name> [from <address>:<port>] [with pose of <delay> second]
  84. If optional delay is specified (not None), it waits for the specified
  85. seconds after sending the AXFR query before starting receiving
  86. responses. This emulates a slower AXFR client.
  87. """
  88. class CustomTransferResult:
  89. """Store transfer result only on the number of received answer RRs.
  90. To be compatible with TransferResult it stores the result in the
  91. 'records' attribute, which is a list. But its content is
  92. meaningless; its only use is to be used with
  93. check_transfer_result_count where its length is of concern.
  94. """
  95. def __init__(self):
  96. self.records = []
  97. # Build arguments and run xfr-client.py. On success, it simply dumps
  98. # the number of received answer RRs to stdout.
  99. (address, port) = parse_addr_port(address, port)
  100. args = ['/bin/sh', 'run_python-tool.sh', 'tools/xfr-client.py',
  101. '-s', address, '-p', str(port)]
  102. if delay is not None:
  103. args.extend(['-d', delay])
  104. args.append(zone_name)
  105. client = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
  106. subprocess.PIPE)
  107. (stdout, stderr) = client.communicate()
  108. result = client.returncode
  109. world.last_client_stdout = stdout
  110. world.last_client_stderr = stderr
  111. assert result == 0, "xfr-client exit code: " + str(result) +\
  112. "\nstdout:\n" + str(stdout) +\
  113. "stderr:\n" + str(stderr)
  114. num_rrs = int(stdout.strip())
  115. # Make the result object, storing dummy value (None) for the number of
  116. # answer RRs in the records list.
  117. world.transfer_result = CustomTransferResult()
  118. world.transfer_result.records = [None for _ in range(0, num_rrs)]
  119. @step('An IXFR transfer of ([\w.]+) (\d+)(?: from ([^:]+)(?::([0-9]+))?)?(?: over (tcp|udp))?')
  120. def perform_ixfr(step, zone_name, serial, address, port, protocol):
  121. """
  122. Perform an IXFR transfer, and store the result as an instance of
  123. TransferResult in world.transfer_result.
  124. Step definition:
  125. An IXFR transfer of <zone_name> <serial> [from <address>:port] [over <tcp|udp>]
  126. Address defaults to 127.0.0.1
  127. Port defaults to 47806
  128. If either tcp or udp is specified, only this protocol will be used.
  129. """
  130. if address is None:
  131. address = "127.0.0.1"
  132. if port is None:
  133. port = 47806
  134. args = [ 'dig', 'IXFR=' + str(serial), '@' + str(address), '-p', str(port), zone_name ]
  135. if protocol is not None:
  136. assert protocol == 'tcp' or protocol == 'udp', "Unknown protocol: " + protocol
  137. if protocol == 'tcp':
  138. args.append('+tcp')
  139. elif protocol == 'udp':
  140. args.append('+notcp')
  141. world.transfer_result = TransferResult(args)
  142. @step('transfer result should have (\d+) rrs?')
  143. def check_transfer_result_count(step, number_of_rrs):
  144. """
  145. Check the number of rrs in the transfer result object created by
  146. the AXFR transfer or IXFR transfer step.
  147. Step definition:
  148. transfer result should have <number> rr[s]
  149. Fails if the number of RRs is not equal to number
  150. """
  151. assert int(number_of_rrs) == len(world.transfer_result.records),\
  152. "Got " + str(len(world.transfer_result.records)) +\
  153. " records, expected " + str(number_of_rrs)
  154. @step('full result of the last transfer should be')
  155. def check_full_transfer_result(step):
  156. """
  157. Check the complete output from the last transfer call.
  158. Step definition:
  159. full result of the last transfer should be <multiline value>
  160. Whitespace is normalized in both the multiline value and the
  161. output, but the order of the output is not.
  162. Fails if there is any difference between the two. Prints
  163. full output and expected value upon failure.
  164. """
  165. records_string = "\n".join(world.transfer_result.records)
  166. records_string = re.sub("[ \t]+", " ", records_string)
  167. expect = re.sub("[ \t]+", " ", step.multiline)
  168. assert records_string.strip() == expect.strip(),\
  169. "Got:\n'" + records_string + "'\nExpected:\n'" + expect + "'"