Add a client for querying nameservers
Change-Id: I2d2eedcd162e7aeac4f3c9c92342bff448b4a5f5
diff --git a/designate_tempest_plugin/clients.py b/designate_tempest_plugin/clients.py
index 9e64b15..8450c5a 100644
--- a/designate_tempest_plugin/clients.py
+++ b/designate_tempest_plugin/clients.py
@@ -30,6 +30,8 @@
PoolClient
from designate_tempest_plugin.services.dns.v2.json.tld_client import \
TldClient
+from designate_tempest_plugin.services.dns.query.query_client import \
+ QueryClient
CONF = config.CONF
@@ -60,3 +62,9 @@
**params)
self.tld_client = TldClient(self.auth_provider,
**params)
+ self.query_client = QueryClient(
+ nameservers=CONF.dns.nameservers,
+ query_timeout=CONF.dns.query_timeout,
+ build_interval=CONF.dns.build_interval,
+ build_timeout=CONF.dns.build_timeout,
+ )
diff --git a/designate_tempest_plugin/common/waiters.py b/designate_tempest_plugin/common/waiters.py
index dbdbd5a..efd3625 100644
--- a/designate_tempest_plugin/common/waiters.py
+++ b/designate_tempest_plugin/common/waiters.py
@@ -147,4 +147,49 @@
if caller:
message = '(%s) %s' % (caller, message)
- raise lib_exc.TimeoutException(message)
\ No newline at end of file
+ raise lib_exc.TimeoutException(message)
+
+
+def wait_for_query(client, name, rdatatype, found=True):
+ """Query nameservers until the record of the given name and type is found.
+
+ :param client: A QueryClient
+ :param name: The record name for which to query
+ :param rdatatype: The record type for which to query
+ :param found: If True, wait until the record is found. Else, wait until the
+ record disappears.
+ """
+ state = "found" if found else "removed"
+ LOG.info("Waiting for record %s of type %s to be %s on nameservers %s",
+ name, rdatatype, state, client.nameservers)
+ start = int(time.time())
+
+ while True:
+ time.sleep(client.build_interval)
+
+ responses = client.query(name, rdatatype)
+ if found:
+ all_answers_good = all(r.answer for r in responses)
+ else:
+ all_answers_good = all(not r.answer for r in responses)
+
+ if not client.nameservers or all_answers_good:
+ LOG.info("Record %s of type %s was successfully %s on nameservers "
+ "%s", name, rdatatype, state, client.nameservers)
+ return
+
+ if int(time.time()) - start >= client.build_timeout:
+ message = ('Record %(name)s of type %(rdatatype)s not %(state)s '
+ 'on nameservers %(nameservers)s within the required '
+ 'time (%(timeout)s s)' %
+ {'name': name,
+ 'rdatatype': rdatatype,
+ 'state': state,
+ 'nameservers': client.nameservers,
+ 'timeout': client.build_timeout})
+
+ caller = misc_utils.find_test_caller()
+ if caller:
+ message = "(%s) %s" % (caller, message)
+
+ raise lib_exc.TimeoutException(message)
diff --git a/designate_tempest_plugin/config.py b/designate_tempest_plugin/config.py
index 07e96b8..d813caa 100644
--- a/designate_tempest_plugin/config.py
+++ b/designate_tempest_plugin/config.py
@@ -34,5 +34,11 @@
cfg.IntOpt('min_ttl',
default=1,
help="The minimum value to respect when generating ttls"),
+ cfg.ListOpt('nameservers',
+ default=[],
+ help="The nameservers to check for change going live"),
+ cfg.IntOpt('query_timeout',
+ default=1,
+ help="The timeout on a single dns query to a nameserver"),
]
diff --git a/designate_tempest_plugin/services/dns/query/__init__.py b/designate_tempest_plugin/services/dns/query/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/query/__init__.py
diff --git a/designate_tempest_plugin/services/dns/query/query_client.py b/designate_tempest_plugin/services/dns/query/query_client.py
new file mode 100644
index 0000000..c4df36a
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/query/query_client.py
@@ -0,0 +1,82 @@
+# Copyright 2016 Rackspace
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import dns
+import dns.exception
+import dns.query
+from tempest import config
+
+CONF = config.CONF
+
+
+class QueryClient(object):
+ """A client which queries multiple nameservers"""
+
+ def __init__(self, nameservers=None, query_timeout=None,
+ build_interval=None, build_timeout=None):
+ self.nameservers = nameservers or []
+ self.query_timeout = query_timeout or CONF.dns.query_timeout
+ self.build_interval = build_interval or CONF.dns.build_interval
+ self.build_timeout = build_timeout or CONF.dns.build_timeout
+
+ self.clients = [SingleQueryClient(ns, query_timeout=query_timeout)
+ for ns in nameservers]
+
+ def query(self, zone_name, rdatatype):
+ return [c.query(zone_name, rdatatype) for c in self.clients]
+
+
+class SingleQueryClient(object):
+ """A client which queries a single nameserver"""
+
+ def __init__(self, nameserver, query_timeout):
+ self.nameserver = Nameserver(nameserver)
+ self.query_timeout = query_timeout
+
+ def query(self, name, rdatatype):
+ return self._dig(name, rdatatype, self.nameserver.ip,
+ self.nameserver.port, timeout=self.query_timeout)
+
+ @classmethod
+ def _prepare_query(cls, zone_name, rdatatype):
+ # support plain strings: "SOA", "A"
+ if isinstance(rdatatype, basestring):
+ rdatatype = dns.rdatatype.from_text(rdatatype)
+ dns_message = dns.message.make_query(zone_name, rdatatype)
+ dns_message.set_opcode(dns.opcode.QUERY)
+ return dns_message
+
+ @classmethod
+ def _dig(cls, name, rdatatype, ip, port, timeout):
+ query = cls._prepare_query(name, rdatatype)
+ return dns.query.udp(query, ip, port=port, timeout=timeout)
+
+
+class Nameserver(object):
+
+ def __init__(self, ip, port=53):
+ self.ip = ip
+ self.port = port
+
+ def __str__(self):
+ return "%s:%s" % (self.ip, self.port)
+
+ def __repr__(self):
+ return str(self)
+
+ @classmethod
+ def from_str(self, nameserver):
+ if ':' in nameserver:
+ ip, port = nameserver.split(':')
+ return Nameserver(ip, int(port))
+ return Nameserver(nameserver)
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_zones.py b/designate_tempest_plugin/tests/scenario/v2/test_zones.py
index ea807d8..91baeb0 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_zones.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_zones.py
@@ -27,6 +27,7 @@
super(ZonesTest, cls).setup_clients()
cls.client = cls.os.zones_client
+ cls.query_client = cls.os.query_client
@test.attr(type='slow')
@test.idempotent_id('d0648f53-4114-45bd-8792-462a82f69d32')
@@ -80,3 +81,31 @@
self.assertEqual('PENDING', zone['status'])
waiters.wait_for_zone_404(self.client, zone['id'])
+
+ @test.attr(type='slow')
+ @test.idempotent_id('ad8d1f5b-da66-46a0-bbee-14dc84a5d791')
+ def test_zone_create_propagates_to_nameservers(self):
+ LOG.info('Create a zone')
+ _, zone = self.client.create_zone()
+ self.addCleanup(self.client.delete_zone, zone['id'])
+
+ waiters.wait_for_zone_status(self.client, zone['id'], "ACTIVE")
+ waiters.wait_for_query(self.query_client, zone['name'], "SOA")
+
+ @test.attr(type='slow')
+ @test.idempotent_id('d13d3095-c78f-4aae-8fe3-a74ccc335c84')
+ def test_zone_delete_propagates_to_nameservers(self):
+ LOG.info('Create a zone')
+ _, zone = self.client.create_zone()
+ self.addCleanup(self.client.delete_zone, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ waiters.wait_for_zone_status(self.client, zone['id'], "ACTIVE")
+ waiters.wait_for_query(self.query_client, zone['name'], "SOA")
+
+ LOG.info('Delete the zone')
+ self.client.delete_zone(zone['id'])
+
+ waiters.wait_for_zone_404(self.client, zone['id'])
+ waiters.wait_for_query(self.query_client, zone['name'], "SOA",
+ found=False)