Merge "Re-enable zone export list filter test"
diff --git a/.zuul.yaml b/.zuul.yaml
index 9cc66cf..02cd904 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,6 +4,7 @@
- check-requirements
- publish-openstack-docs-pti
- tempest-plugin-jobs
+ - release-notes-jobs-python3
check:
jobs:
- neutron-tempest-plugin-designate-scenario
diff --git a/designate_tempest_plugin/clients.py b/designate_tempest_plugin/clients.py
index 6fbfc05..deafecb 100644
--- a/designate_tempest_plugin/clients.py
+++ b/designate_tempest_plugin/clients.py
@@ -51,6 +51,7 @@
import SevriceClient
from designate_tempest_plugin.services.dns.v2.json.designate_limit_client \
import DesignateLimitClient
+from designate_tempest_plugin.services.dns.v2.json.ptr_client import PtrClient
CONF = config.CONF
@@ -99,6 +100,7 @@
self.tsigkey_client = TsigkeyClient(**params)
self.service_client = SevriceClient(**params)
self.designate_limit_client = DesignateLimitClient(**params)
+ self.ptr_client = PtrClient(**params)
self.query_client = QueryClient(
nameservers=CONF.dns.nameservers,
diff --git a/designate_tempest_plugin/services/dns/json/base.py b/designate_tempest_plugin/services/dns/json/base.py
index 1f12642..cbfb34f 100644
--- a/designate_tempest_plugin/services/dns/json/base.py
+++ b/designate_tempest_plugin/services/dns/json/base.py
@@ -87,17 +87,25 @@
expected_code=expected_code, read_code=int(read_code),
)
- def get_uri(self, resource_name, uuid=None, params=None):
+ def get_uri(self, resource_name, uuid=None, params=None,
+ uuid_prefix_char=None):
"""Get URI for a specific resource or object.
:param resource_name: The name of the REST resource, e.g., 'zones'.
:param uuid: The unique identifier of an object in UUID format.
:param params: A Python dict that represents the query paramaters to
include in the request URI.
+ :param uuid_prefix_char: applies to override hardcoded ('/')
+ prefix UUID character. This parameter enables to set required
+ by API character, for example ":" instead of "/".
:returns: Relative URI for the resource or object.
"""
uri_pattern = '{pref}/{res}{uuid}{params}'
- uuid = '/%s' % uuid if uuid else ''
+ if uuid_prefix_char:
+ uuid = uuid_prefix_char + '%s' % uuid if uuid else ''
+ else:
+ uuid = '/%s' % uuid if uuid else ''
+
params = '?%s' % urllib.urlencode(params) if params else ''
return uri_pattern.format(pref=self.uri_prefix,
@@ -141,7 +149,7 @@
return resp, self.deserialize(resp, body)
def _show_request(self, resource, uuid, headers=None, params=None,
- extra_headers=False):
+ extra_headers=False, uuid_prefix_char=None):
"""Gets a specific object of the specified type.
:param resource: The name of the REST resource, e.g., 'zones'.
:param uuid: Unique identifier of the object in UUID format.
@@ -152,9 +160,13 @@
method are to be used but additional
headers are needed in the request
pass them in as a dict.
+ :param uuid_prefix_char: applies to override hardcoded ('/')
+ prefix UUID character. This parameter enables to set required
+ by API character, for example ":" instead of "/".
:returns: Serialized object as a dictionary.
"""
- uri = self.get_uri(resource, uuid=uuid, params=params)
+ uri = self.get_uri(resource, uuid=uuid, params=params,
+ uuid_prefix_char=uuid_prefix_char)
resp, body = self.get(
uri, headers=headers, extra_headers=extra_headers)
@@ -199,7 +211,7 @@
return resp, self.deserialize(resp, body)
def _update_request(self, resource, uuid, data, params=None, headers=None,
- extra_headers=False):
+ extra_headers=False, uuid_prefix_char=None):
"""Updates the specified object using PATCH request.
:param resource: The name of the REST resource, e.g., 'zones'
:param uuid: Unique identifier of the object in UUID format.
@@ -214,13 +226,18 @@
method are to be used but additional
headers are needed in the request
pass them in as a dict.
+ :param uuid_prefix_char: applies to override hardcoded ('/')
+ prefix UUID character. This parameter enables to set required
+ by API character, for example ":" instead of "/".
:returns: Serialized object as a dictionary.
"""
body = self.serialize(data)
- uri = self.get_uri(resource, uuid=uuid, params=params)
+ uri = self.get_uri(
+ resource, uuid=uuid, params=params,
+ uuid_prefix_char=uuid_prefix_char)
resp, body = self.patch(uri, body=body,
- headers=headers, extra_headers=True)
+ headers=headers, extra_headers=extra_headers)
self.expected_success(self.UPDATE_STATUS_CODES, resp.status)
diff --git a/designate_tempest_plugin/services/dns/v2/json/ptr_client.py b/designate_tempest_plugin/services/dns/v2/json/ptr_client.py
new file mode 100644
index 0000000..1bd59b3
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/v2/json/ptr_client.py
@@ -0,0 +1,85 @@
+# Copyright 2021 Red Hat.
+#
+# 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.
+from tempest.lib.common.utils import data_utils
+
+from designate_tempest_plugin import data_utils as dns_data_utils
+from designate_tempest_plugin.services.dns.v2.json import base
+from tempest import config
+
+CONF = config.CONF
+
+
+class PtrClient(base.DnsClientV2Base):
+
+ @base.handle_errors
+ def set_ptr_record(self, floatingip_id, ptr_name=None,
+ ttl=None, description=None, headers=None):
+ """Set a PTR record for the given FloatingIP
+
+ :param floatingip_id: valid UUID of floating IP to be used.
+ :param ptr_name PTR record name or random if not provided.
+ :param ttl TTL or random valid value if not provided.
+ :param description Description or random if not provided.
+ :param headers (dict): The headers to use for the request.
+ :return: created PTR dictionary.
+ """
+ ptr = {
+ 'ptrdname': ptr_name or dns_data_utils.rand_zone_name(),
+ 'ttl': ttl or dns_data_utils.rand_ttl(),
+ 'description': description or data_utils.rand_name(
+ 'test-ptr')}
+
+ return self._update_request(
+ resource='reverse/floatingips/{}'.format(CONF.identity.region),
+ uuid=floatingip_id, data=ptr, headers=headers,
+ uuid_prefix_char=':')[1]
+
+ @base.handle_errors
+ def show_ptr_record(self, floatingip_id, headers=None):
+ """Show PTR record for the given FloatingIP
+
+ :param floatingip_id: valid UUID of floating IP to show.
+ :param headers (dict): The headers to use for the request.
+ :return: Shown PTR dictionary.
+ """
+ return self._show_request(
+ resource='reverse/floatingips/{}'.format(CONF.identity.region),
+ uuid=floatingip_id, headers=headers, uuid_prefix_char=':')[1]
+
+ @base.handle_errors
+ def list_ptr_records(self, headers=None):
+ """List PTR records for the given FloatingIP
+
+ :param headers (dict): The headers to use for the request.
+ :return: List of PTR records.
+ """
+ return self._list_request(
+ 'reverse/floatingips', headers=headers)[1]['floatingips']
+
+ @base.handle_errors
+ def unset_ptr_record(self, floatingip_id, headers=None):
+ """Unset the PTR record for a given FloatingIP
+
+ :param floatingip_id: valid UUID of floating IP to unset.
+ :param headers (dict): The headers to use for the request.
+ :return: Tuple (Response, Body)
+ """
+ data = {"ptrdname": None}
+ resp, body = self._update_request(
+ resource='reverse/floatingips/{}'.format(CONF.identity.region),
+ uuid=floatingip_id, data=data, headers=headers,
+ uuid_prefix_char=':')
+ # Unset PTR should Return a HTTP 202
+ self.expected_success(202, resp.status)
+ return resp, body
diff --git a/designate_tempest_plugin/services/dns/v2/json/transfer_accepts_client.py b/designate_tempest_plugin/services/dns/v2/json/transfer_accepts_client.py
index a0612e9..e09f775 100644
--- a/designate_tempest_plugin/services/dns/v2/json/transfer_accepts_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/transfer_accepts_client.py
@@ -19,18 +19,20 @@
@base.handle_errors
def create_transfer_accept(self, transfer_accept_data,
- params=None):
+ params=None, headers=None):
"""Create a zone transfer_accept.
:param transfer_accept_data: A python dictionary representing
data for the zone transfer accept.
:param params: A Python dict that represents the query paramaters to
include in the accept URI.
+ :param headers (dict): The headers to use for the request.
:return: Serialized accepted zone transfer as a dictionary.
"""
transfer_accept_uri = 'zones/tasks/transfer_accepts'
resp, body = self._create_request(
- transfer_accept_uri, transfer_accept_data, params=params)
+ transfer_accept_uri, transfer_accept_data,
+ params=params, headers=headers)
# Create Transfer accept should Return a HTTP 201
self.expected_success(201, resp.status)
@@ -38,15 +40,17 @@
return resp, body
@base.handle_errors
- def show_transfer_accept(self, uuid, params=None):
+ def show_transfer_accept(self, uuid, params=None, headers=None):
"""Gets a specific accepted zone transfer..
:param uuid: Unique identifier of the transfer_accept.
:param params: A Python dict that represents the query paramaters to
include in the accept URI.
+ :param headers (dict): The headers to use for the request.
:return: Serialized accepted zone transfer as a dictionary.
"""
return self._show_request(
- 'zones/tasks/transfer_accepts', uuid, params=params)
+ 'zones/tasks/transfer_accepts', uuid,
+ params=params, headers=headers)
@base.handle_errors
def list_transfer_accept(self, params=None, headers=None):
diff --git a/designate_tempest_plugin/services/dns/v2/json/zones_client.py b/designate_tempest_plugin/services/dns/v2/json/zones_client.py
index 04a628b..4becb58 100644
--- a/designate_tempest_plugin/services/dns/v2/json/zones_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/zones_client.py
@@ -121,14 +121,16 @@
return self._list_request('zones', params=params, headers=headers)
@base.handle_errors
- def delete_zone(self, uuid, params=None):
+ def delete_zone(self, uuid, params=None, headers=None):
"""Deletes a zone having the specified UUID.
:param uuid: The unique identifier of the zone.
:param params: A Python dict that represents the query paramaters to
include in the request URI.
+ :param headers (dict): The headers to use for the request.
:return: A tuple with the server response and the response body.
"""
- resp, body = self._delete_request('zones', uuid, params=params)
+ resp, body = self._delete_request(
+ 'zones', uuid, params=params, headers=headers)
# Delete Zone should Return a HTTP 202
self.expected_success(202, resp.status)
diff --git a/designate_tempest_plugin/tests/api/v2/test_pool.py b/designate_tempest_plugin/tests/api/v2/test_pool.py
index d127006..e2516af 100644
--- a/designate_tempest_plugin/tests/api/v2/test_pool.py
+++ b/designate_tempest_plugin/tests/api/v2/test_pool.py
@@ -208,3 +208,79 @@
self.assertEqual("invalid_uuid", resp_body['type'])
self.assertEqual("Invalid UUID pool_id: foo",
resp_body['message'])
+
+
+class TestPoolAdminNegative(BasePoolTest):
+
+ credentials = ["admin"]
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(TestPoolAdminNegative, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(TestPoolAdminNegative, cls).setup_clients()
+ cls.admin_client = cls.os_admin.pool_client
+
+ @decorators.idempotent_id('0a8cdc1e-ac02-11eb-ae06-74e5f9e2a801')
+ def test_create_pool_invalid_name(self):
+ LOG.info('Create a pool using a huge size string for name)')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+ self.admin_client.create_pool(
+ pool_name=data_utils.rand_name(name="Huge_size_name") * 10000)
+
+ @decorators.idempotent_id('9a787d0e-ac04-11eb-ae06-74e5f9e2a801')
+ def test_create_pool_invalid_hostname_in_ns_records(self):
+ LOG.info('Create a pool using invalid hostname in ns_records')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+ self.admin_client.create_pool(
+ ns_records=[{"hostname": "ns1_example_org_", "priority": 1}])
+
+ @decorators.idempotent_id('9a787d0e-ac04-11eb-ae06-74e5f9e2a801')
+ def test_create_pool_invalid_priority_in_ns_records(self):
+ LOG.info('Create a pool using invalid priority in ns_records')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+ self.admin_client.create_pool(
+ ns_records=[{"hostname": "ns1.example.org.", "priority": -1}])
+
+ @decorators.idempotent_id('cc378e4c-ac05-11eb-ae06-74e5f9e2a801')
+ # Note: Update pool API is deprecated for removal.
+ def test_update_pool_with_invalid_name(self):
+ LOG.info('Create a pool')
+ pool = self.admin_client.create_pool()[1]
+ self.addCleanup(self.admin_client.delete_pool, pool['id'])
+
+ LOG.info('Update the pool using a name that is too long')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+ self.admin_client.update_pool(
+ pool['id'],
+ pool_name=data_utils.rand_name(name="Huge_size_name") * 10000)
+
+ @decorators.idempotent_id('2e496596-ac07-11eb-ae06-74e5f9e2a801')
+ def test_update_pool_with_invalid_hostname_in_ns_records(self):
+ # Note: Update pool API is deprecated for removal.
+ LOG.info('Create a pool')
+ pool = self.admin_client.create_pool()[1]
+ self.addCleanup(self.admin_client.delete_pool, pool['id'])
+
+ LOG.info('Update the pool using invalid hostname in ns_records')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+ self.admin_client.update_pool(
+ pool['id'],
+ ns_records=[{"hostname": "ns1_example_org_", "priority": 1}])
+
+ @decorators.idempotent_id('3e934624-ac07-11eb-ae06-74e5f9e2a801')
+ def test_update_pool_with_invalid_priority_in_ns_records(self):
+ # Note: Update pool API is deprecated for removal.
+ LOG.info('Create a pool')
+ pool = self.admin_client.create_pool()[1]
+ self.addCleanup(self.admin_client.delete_pool, pool['id'])
+
+ LOG.info('Update the pool using invalid priority in ns_records')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+ self.admin_client.update_pool(
+ pool['id'],
+ ns_records=[{"hostname": "ns1.example.org.", "priority": -1}])
diff --git a/designate_tempest_plugin/tests/api/v2/test_ptrs.py b/designate_tempest_plugin/tests/api/v2/test_ptrs.py
new file mode 100644
index 0000000..cfd9685
--- /dev/null
+++ b/designate_tempest_plugin/tests/api/v2/test_ptrs.py
@@ -0,0 +1,125 @@
+# Copyright 2021 Red Hat.
+#
+# 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.
+from oslo_log import log as logging
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from designate_tempest_plugin.tests import base
+import tempest.test
+
+LOG = logging.getLogger(__name__)
+
+CONF = config.CONF
+
+
+class BasePtrTest(base.BaseDnsV2Test):
+ excluded_keys = ['created_at', 'updated_at', 'version', 'links',
+ 'status', 'action']
+
+
+class DesignatePtrRecord(BasePtrTest, tempest.test.BaseTestCase):
+ credentials = ['primary']
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(DesignatePtrRecord, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(DesignatePtrRecord, cls).setup_clients()
+ cls.primary_ptr_client = cls.os_primary.ptr_client
+ cls.primary_floating_ip_client = cls.os_primary.floating_ips_client
+
+ def _set_ptr(self):
+ fip = self.primary_floating_ip_client.create_floatingip(
+ floating_network_id=CONF.network.public_network_id)['floatingip']
+ fip_id = fip['id']
+ self.addCleanup(self.primary_floating_ip_client.delete_floatingip,
+ fip_id)
+ ptr = self.primary_ptr_client.set_ptr_record(fip_id)
+ self.addCleanup(self.primary_ptr_client.unset_ptr_record, fip_id)
+ self.assertEqual('CREATE', ptr['action'])
+ self.assertEqual('PENDING', ptr['status'])
+ return fip_id, ptr
+
+ @decorators.idempotent_id('2fb9d6ea-871d-11eb-9f9a-74e5f9e2a801')
+ def test_set_floatingip_ptr(self):
+ self._set_ptr()
+
+ @decorators.idempotent_id('9179325a-87d0-11eb-9f9a-74e5f9e2a801')
+ def test_show_floatingip_ptr(self):
+ fip_id, ptr = self._set_ptr()
+ show_ptr = self.primary_ptr_client.show_ptr_record(
+ floatingip_id=fip_id)
+ self.assertExpected(ptr, show_ptr, self.excluded_keys)
+
+ @decorators.idempotent_id('9187a9c6-87d4-11eb-9f9a-74e5f9e2a801')
+ def test_list_floatingip_ptr_records(self):
+ number_of_ptr_records = 3
+ created_ptr_ids = []
+ for _ in range(number_of_ptr_records):
+ fip_id, ptr = self._set_ptr()
+ created_ptr_ids.append(ptr['id'])
+ received_ptr_ids = sorted(
+ [item['id'] for item in
+ self.primary_ptr_client.list_ptr_records()])
+ self.assertEqual(
+ sorted(created_ptr_ids), received_ptr_ids,
+ 'Failed - received PTR IDs: {} are not as'
+ ' expected: {}'.format(created_ptr_ids, received_ptr_ids))
+
+ @decorators.idempotent_id('499b5a7e-87e1-11eb-b412-74e5f9e2a801')
+ def test_unset_floatingip_ptr(self):
+ fip_id, ptr = self._set_ptr()
+ self.primary_ptr_client.unset_ptr_record(fip_id)
+
+
+class DesignatePtrRecordNegative(BasePtrTest, tempest.test.BaseTestCase):
+ credentials = ['primary']
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(DesignatePtrRecordNegative, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(DesignatePtrRecordNegative, cls).setup_clients()
+ cls.primary_ptr_client = cls.os_primary.ptr_client
+ cls.primary_floating_ip_client = cls.os_primary.floating_ips_client
+
+ def _set_ptr(self, ptr_name=None, ttl=None, description=None,
+ headers=None):
+ fip = self.primary_floating_ip_client.create_floatingip(
+ floating_network_id=CONF.network.public_network_id)[
+ 'floatingip']
+ fip_id = fip['id']
+ self.addCleanup(self.primary_floating_ip_client.delete_floatingip,
+ fip_id)
+ ptr = self.primary_ptr_client.set_ptr_record(
+ fip_id, ptr_name=ptr_name, ttl=ttl, description=description,
+ headers=headers)
+ self.addCleanup(self.primary_ptr_client.unset_ptr_record, fip_id)
+ self.assertEqual('CREATE', ptr['action'])
+ self.assertEqual('PENDING', ptr['status'])
+ return fip_id, ptr
+
+ def test_set_floatingip_ptr_invalid_ttl(self):
+ LOG.info('Try to set PTR record using invalid TTL value')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_object', 400):
+ self._set_ptr(ttl=-10)
diff --git a/designate_tempest_plugin/tests/api/v2/test_recordset.py b/designate_tempest_plugin/tests/api/v2/test_recordset.py
index b8f59fb..96179f1 100644
--- a/designate_tempest_plugin/tests/api/v2/test_recordset.py
+++ b/designate_tempest_plugin/tests/api/v2/test_recordset.py
@@ -281,10 +281,14 @@
LOG.info('Re-Fetch Recordsets as Admin tenant for a Primary project '
'using "x-auth-all-projects" HTTP header.')
+ # Note: This is an all-projects list call, so other tests running
+ # in parallel will impact the list result set. Since the default
+ # pagination limit is only 20, we set a param limit of 1000 here.
primary_recordsets_ids = [
item['id'] for item in self.admin_client.list_recordset(
self.zone['id'],
- headers={'x-auth-all-projects': True})[1]['recordsets']]
+ headers={'x-auth-all-projects': True},
+ params={'limit': 1000})[1]['recordsets']]
for recordset_id in [body_pr_1['id'], body_pr_2['id']]:
self.assertIn(
@@ -557,9 +561,8 @@
cls.alt_zone_client = cls.os_alt.zones_client
cls.admin_client = cls.os_admin.recordset_client
- def _create_client_recordset(self, clients_list=None):
+ def _create_client_recordset(self, clients_list):
"""Create a zone and asoociated recordset using given credentials
-
:param clients_list: supported credentials are: 'primary' and 'alt'.
:return: dictionary of created recordsets.
"""
@@ -703,8 +706,11 @@
project_ids_used = [
item['project_id'] for item in self._create_client_recordset(
['primary', 'alt']).values()]
+ # Note: This is an all-projects list call, so other tests running
+ # in parallel will impact the list result set. Since the default
+ # pagination limit is only 20, we set a param limit of 1000 here.
recordsets = self.admin_client.list_owned_recordsets(
- headers={'x-auth-all-projects': True})
+ headers={'x-auth-all-projects': True}, params={'limit': 1000})
LOG.info('Received by API recordsets are {} '.format(recordsets))
project_ids_api = set([item['project_id'] for item in recordsets])
for prj_id in project_ids_used:
diff --git a/designate_tempest_plugin/tests/api/v2/test_tld.py b/designate_tempest_plugin/tests/api/v2/test_tld.py
index 380d224..f14d4ed 100644
--- a/designate_tempest_plugin/tests/api/v2/test_tld.py
+++ b/designate_tempest_plugin/tests/api/v2/test_tld.py
@@ -43,8 +43,14 @@
@classmethod
def resource_setup(cls):
super(TldAdminTest, cls).resource_setup()
- cls.tld = cls.admin_client.create_tld(tld_name='com',
- ignore_errors=lib_exc.Conflict)
+ cls.tld = cls.admin_client.create_tld(
+ tld_name='com', ignore_errors=lib_exc.Conflict
+ )
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_client.delete_tld(cls.tld[1]['id'])
+ super(TldAdminTest, cls).resource_cleanup()
@decorators.idempotent_id('52a4bb4b-4eff-4591-9dd3-ad98316806c3')
def test_create_tld(self):
diff --git a/designate_tempest_plugin/tests/api/v2/test_transfer_accepts.py b/designate_tempest_plugin/tests/api/v2/test_transfer_accepts.py
index 1f7c1e1..7cfc4a9 100644
--- a/designate_tempest_plugin/tests/api/v2/test_transfer_accepts.py
+++ b/designate_tempest_plugin/tests/api/v2/test_transfer_accepts.py
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
+from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
@@ -56,15 +57,20 @@
@decorators.idempotent_id('1c6baf97-a83e-4d2e-a5d8-9d37fb7808f3')
def test_create_transfer_accept(self):
LOG.info('Create a zone')
- _, zone = self.prm_zone_client.create_zone()
+ _, zone = self.prm_zone_client.create_zone(wait_until='ACTIVE')
self.addCleanup(
- self.wait_zone_delete, self.prm_zone_client, zone['id'])
+ self.wait_zone_delete, self.admin_zone_client, zone['id'],
+ headers={'x-auth-all-projects': True},
+ ignore_errors=lib_exc.NotFound)
LOG.info('Create a zone transfer_request')
_, transfer_request = self.prm_request_client.create_transfer_request(
zone['id'])
- self.addCleanup(self.prm_request_client.delete_transfer_request,
- transfer_request['id'])
+ self.addCleanup(
+ self.transfer_request_delete,
+ self.prm_request_client,
+ transfer_request['id']
+ )
data = {
"key": transfer_request['key'],
@@ -80,15 +86,20 @@
@decorators.idempotent_id('37c6afbb-3ea3-4fd8-94ea-a426244f019a')
def test_show_transfer_accept(self):
LOG.info('Create a zone')
- _, zone = self.prm_zone_client.create_zone()
+ _, zone = self.prm_zone_client.create_zone(wait_until='ACTIVE')
self.addCleanup(
- self.wait_zone_delete, self.prm_zone_client, zone['id'])
+ self.wait_zone_delete, self.admin_zone_client, zone['id'],
+ headers={'x-auth-all-projects': True},
+ ignore_errors=lib_exc.NotFound)
LOG.info('Create a zone transfer_request')
_, transfer_request = self.prm_request_client.create_transfer_request(
zone['id'])
- self.addCleanup(self.prm_request_client.delete_transfer_request,
- transfer_request['id'])
+ self.addCleanup(
+ self.transfer_request_delete,
+ self.prm_request_client,
+ transfer_request['id']
+ )
data = {
"key": transfer_request['key'],
@@ -111,15 +122,20 @@
def test_ownership_transferred_zone(self):
LOG.info('Create a Primary zone')
- zone = self.prm_zone_client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.prm_zone_client,
- zone['id'], ignore_errors=lib_exc.NotFound)
+ zone = self.prm_zone_client.create_zone(wait_until='ACTIVE')[1]
+ self.addCleanup(
+ self.wait_zone_delete, self.admin_zone_client, zone['id'],
+ headers={'x-auth-all-projects': True},
+ ignore_errors=lib_exc.NotFound)
LOG.info('Create a Primary zone transfer_request')
transfer_request = self.prm_request_client.create_transfer_request(
zone['id'])[1]
- self.addCleanup(self.prm_request_client.delete_transfer_request,
- transfer_request['id'])
+ self.addCleanup(
+ self.transfer_request_delete,
+ self.prm_request_client,
+ transfer_request['id']
+ )
data = {
"key": transfer_request['key'],
@@ -151,15 +167,20 @@
for _ in range(number_of_zones_to_transfer):
LOG.info('Create a Primary zone')
- zone = self.prm_zone_client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.prm_zone_client,
- zone['id'], ignore_errors=lib_exc.NotFound)
+ zone = self.prm_zone_client.create_zone(wait_until='ACTIVE')[1]
+ self.addCleanup(
+ self.wait_zone_delete, self.admin_zone_client, zone['id'],
+ headers={'x-auth-all-projects': True},
+ ignore_errors=lib_exc.NotFound)
LOG.info('Create a Primary zone transfer_request')
transfer_request = self.prm_request_client.create_transfer_request(
zone['id'])[1]
- self.addCleanup(self.prm_request_client.delete_transfer_request,
- transfer_request['id'])
+ self.addCleanup(
+ self.transfer_request_delete,
+ self.prm_request_client,
+ transfer_request['id']
+ )
data = {
"key": transfer_request['key'],
@@ -169,17 +190,20 @@
transfer_accept = self.alt_accept_client.create_transfer_accept(
data)[1]
- LOG.info('Ensure we respond with ACTIVE status')
+ LOG.info('Ensure we respond with COMPLETE status')
self.assertEqual('COMPLETE', transfer_accept['status'])
transfer_request_ids.append(transfer_accept['id'])
# As Admin list all accepted zone transfers, expected:
# each previously transferred zone is listed.
+ # Note: This is an all-projects list call, so other tests running
+ # in parallel will impact the list result set. Since the default
+ # pagination limit is only 20, we set a param limit of 1000 here.
LOG.info('Use Admin client to list all "accepted zone transfers"')
admin_client_accept_ids = [
item['id'] for item in
self.admin_accept_client.list_transfer_accept(
- headers={'x-auth-all-projects': True})]
+ headers={'x-auth-all-projects': True}, params={'limit': 1000})]
for tr_id in transfer_request_ids:
self.assertIn(
tr_id, admin_client_accept_ids,
@@ -216,3 +240,121 @@
admin_client_accept_ids,
"Failed, filtered list should be empty, but actually it's not, "
"filtered IDs:{} ".format(admin_client_accept_ids))
+
+ @decorators.idempotent_id('b6ac770e-a1d3-11eb-b534-74e5f9e2a801')
+ def test_show_transfer_accept_impersonate_another_project(self):
+ LOG.info('Create a zone as primary tenant')
+ zone = self.prm_zone_client.create_zone(wait_until='ACTIVE')[1]
+
+ # In case when something goes wrong with the test and E2E
+ # scenario fails for some reason, we'll use Admin tenant
+ # to activate Cleanup for a zone.
+ # Note: "ignore_errors=lib_exc.NotFound" is used to prevent a
+ # failure in case when E2E scenario was successfully completed.
+ # Means that Alt tenant has already been able to run a cleanup
+ # for a zone.
+ self.addCleanup(
+ self.wait_zone_delete, self.admin_zone_client, zone['id'],
+ headers={'x-auth-all-projects': True},
+ ignore_errors=lib_exc.NotFound)
+
+ LOG.info('Create a zone transfer_request as primary tenant')
+ transfer_request = self.prm_request_client.create_transfer_request(
+ zone['id'])[1]
+ self.addCleanup(
+ self.transfer_request_delete,
+ self.prm_request_client,
+ transfer_request['id']
+ )
+ data = {
+ "key": transfer_request['key'],
+ "zone_transfer_request_id": transfer_request['id']
+ }
+
+ LOG.info('Create a zone transfer_accept for Alt tenant, using '
+ 'Admin client and "sudo" option')
+ transfer_accept = self.admin_accept_client.create_transfer_accept(
+ data, headers={
+ 'x-auth-sudo-project-id': self.os_alt.credentials.project_id,
+ 'content-type': 'application/json'})[1]
+
+ LOG.info('Fetch the transfer_accept as Alt tenant')
+ body = self.alt_accept_client.show_transfer_accept(
+ transfer_accept['id'])[1]
+
+ LOG.info('Ensure the fetched response matches the '
+ 'created transfer_accept')
+ self.assertExpected(transfer_accept, body, self.excluded_keys)
+
+ # E2E accept zone transfer is done, therefore Alt tenant
+ # should be able to "cleanup" a transferred zone.
+ self.addCleanup(
+ self.wait_zone_delete, self.alt_zone_client, zone['id'])
+
+
+class TransferAcceptTestNegative(BaseTransferAcceptTest):
+
+ credentials = ['primary', 'alt', 'admin']
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(TransferAcceptTestNegative, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(TransferAcceptTestNegative, cls).setup_clients()
+ cls.zone_client = cls.os_primary.zones_client
+ cls.request_client = cls.os_primary.transfer_request_client
+ cls.client = cls.os_primary.transfer_accept_client
+
+ @decorators.idempotent_id('324a3e80-a1cc-11eb-b534-74e5f9e2a801')
+ def test_create_transfer_accept_using_invalid_key(self):
+ LOG.info('Create a zone')
+ zone = self.zone_client.create_zone(wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+
+ LOG.info('Create a zone transfer_request')
+ transfer_request = self.request_client.create_transfer_request(
+ zone['id'])[1]
+ self.addCleanup(
+ self.transfer_request_delete,
+ self.request_client,
+ transfer_request['id']
+ )
+
+ data = {"key": data_utils.rand_password(len(transfer_request['key'])),
+ "zone_transfer_request_id": transfer_request['id']}
+
+ LOG.info('Create a zone transfer_accept using invalid key')
+ self.assertRaises(
+ lib_exc.Forbidden, self.client.create_transfer_accept,
+ transfer_accept_data=data)
+
+ @decorators.idempotent_id('23afb948-a1ce-11eb-b534-74e5f9e2a801')
+ def test_create_transfer_accept_using_deleted_transfer_request_id(self):
+ LOG.info('Create a zone')
+ zone = self.zone_client.create_zone(wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+
+ LOG.info('Create a zone transfer_request')
+ transfer_request = self.request_client.create_transfer_request(
+ zone['id'])[1]
+ self.addCleanup(
+ self.transfer_request_delete,
+ self.request_client,
+ transfer_request['id']
+ )
+
+ data = {
+ "key": transfer_request['key'],
+ "zone_transfer_request_id": transfer_request['id']
+ }
+
+ LOG.info('Delete transfer request')
+ self.request_client.delete_transfer_request(transfer_request['id'])
+
+ LOG.info('Ensure 404 when accepting non existing request ID')
+ self.assertRaises(lib_exc.NotFound,
+ lambda: self.client.create_transfer_accept(data))
diff --git a/designate_tempest_plugin/tests/api/v2/test_transfer_request.py b/designate_tempest_plugin/tests/api/v2/test_transfer_request.py
index 666fd79..94a1480 100644
--- a/designate_tempest_plugin/tests/api/v2/test_transfer_request.py
+++ b/designate_tempest_plugin/tests/api/v2/test_transfer_request.py
@@ -228,9 +228,13 @@
LOG.info('List transfer_requests for all projects using Admin tenant '
'and "x-auth-all-projects" HTTP header.')
+ # Note: This is an all-projects list call, so other tests running
+ # in parallel will impact the list result set. Since the default
+ # pagination limit is only 20, we set a param limit of 1000 here.
request_ids = [
item['id'] for item in self.admin_client.list_transfer_requests(
- headers={'x-auth-all-projects': True})[1]['transfer_requests']]
+ headers={'x-auth-all-projects': True},
+ params={'limit': 1000})[1]['transfer_requests']]
for request_id in [primary_transfer_request['id'],
alt_transfer_request['id']]:
diff --git a/designate_tempest_plugin/tests/api/v2/test_zone_tasks.py b/designate_tempest_plugin/tests/api/v2/test_zone_tasks.py
index 5816cd4..12eea8c 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zone_tasks.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zone_tasks.py
@@ -53,9 +53,10 @@
@decorators.idempotent_id('287e2cd0-a0e7-11eb-b962-74e5f9e2a801')
def test_zone_abandon(self):
-
LOG.info('Create a PRIMARY zone')
pr_zone = self.client.create_zone()[1]
+ self.addCleanup(self.wait_zone_delete, self.client, pr_zone['id'])
+ waiters.wait_for_zone_status(self.client, pr_zone['id'], 'ACTIVE')
LOG.info('Ensure we respond with CREATE+PENDING')
self.assertEqual('CREATE', pr_zone['action'])
@@ -85,6 +86,7 @@
LOG.info('Create a PRIMARY zone and add to the cleanup')
pr_zone = self.client.create_zone()[1]
self.addCleanup(self.wait_zone_delete, self.client, pr_zone['id'])
+ waiters.wait_for_zone_status(self.client, pr_zone['id'], 'ACTIVE')
LOG.info('Ensure we respond with CREATE+PENDING')
self.assertEqual('CREATE', pr_zone['action'])
@@ -138,6 +140,7 @@
LOG.info('Create a PRIMARY zone')
pr_zone = self.client.create_zone()[1]
self.addCleanup(self.wait_zone_delete, self.client, pr_zone['id'])
+ waiters.wait_for_zone_status(self.client, pr_zone['id'], 'ACTIVE')
LOG.info('Ensure we respond with CREATE+PENDING')
self.assertEqual('CREATE', pr_zone['action'])
diff --git a/designate_tempest_plugin/tests/api/v2/test_zones.py b/designate_tempest_plugin/tests/api/v2/test_zones.py
index 78b8d4a..c2337fe 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones.py
@@ -270,8 +270,12 @@
self.admin_client, admin_zone['id'], 'ACTIVE')
LOG.info('As admin user list all projects zones')
+ # Note: This is an all-projects list call, so other tests running
+ # in parallel will impact the list result set. Since the default
+ # pagination limit is only 20, we set a param limit of 1000 here.
body = self.admin_client.list_zones(
- headers={'x-auth-all-projects': True})[1]['zones']
+ headers={'x-auth-all-projects': True},
+ params={'limit': 1000})[1]['zones']
listed_zone_ids = [item['id'] for item in body]
LOG.info('Ensure the fetched response includes all zone '
diff --git a/designate_tempest_plugin/tests/api/v2/test_zones_exports.py b/designate_tempest_plugin/tests/api/v2/test_zones_exports.py
index e1b0226..cd5a3ac 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones_exports.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones_exports.py
@@ -146,9 +146,13 @@
self.addCleanup(self.alt_client.delete_zone_export, alt_export['id'])
LOG.info('As admin user list zone exports for all projects')
+ # Note: This is an all-projects list call, so other tests running
+ # in parallel will impact the list result set. Since the default
+ # pagination limit is only 20, we set a param limit of 1000 here.
listed_exports_ids = [
item['id'] for item in self.admin_client.list_zone_exports(
- headers={'x-auth-all-projects': True})[1]['exports']]
+ headers={'x-auth-all-projects': True},
+ params={'limit': 1000})[1]['exports']]
LOG.info('Make sure that all previously created zone '
'export IDs are listed')
diff --git a/designate_tempest_plugin/tests/api/v2/test_zones_imports.py b/designate_tempest_plugin/tests/api/v2/test_zones_imports.py
index 950e021..332899d 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones_imports.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones_imports.py
@@ -62,9 +62,9 @@
LOG.info('Create a zone import')
_, zone_import = self.client.create_zone_import()
self.addCleanup(self.clean_up_resources, zone_import['id'])
-
- LOG.info('Ensure we respond with PENDING')
- self.assertEqual(const.PENDING, zone_import['status'])
+ # Make sure we complete the import and have the zone_id for cleanup
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import['id'], const.COMPLETE)
@decorators.idempotent_id('31eaf25a-9532-11eb-a55d-74e5f9e2a801')
def test_create_zone_import_invalid_ttl(self):
@@ -90,6 +90,9 @@
LOG.info('Create a zone import')
_, zone_import = self.client.create_zone_import()
self.addCleanup(self.clean_up_resources, zone_import['id'])
+ # Make sure we complete the import and have the zone_id for cleanup
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import['id'], const.COMPLETE)
LOG.info('Re-Fetch the zone import')
resp, body = self.client.show_zone_import(zone_import['id'])
@@ -120,6 +123,9 @@
LOG.info('Create a zone import')
_, zone_import = self.client.create_zone_import()
self.addCleanup(self.clean_up_resources, zone_import['id'])
+ # Make sure we complete the import and have the zone_id for cleanup
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import['id'], const.COMPLETE)
LOG.info('List zones imports')
_, body = self.client.list_zone_imports()
@@ -133,8 +139,9 @@
zone_import = self.client.create_zone_import()[1]
self.addCleanup(self.clean_up_resources, zone_import['id'])
- LOG.info('Ensure we respond with PENDING')
- self.assertEqual(const.PENDING, zone_import['status'])
+ # Make sure we complete the import and have the zone_id for cleanup
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import['id'], const.COMPLETE)
LOG.info('Show a zone import for a Primary tenant, using Alt tenant. '
'Expected:404 NotFound')
@@ -173,6 +180,9 @@
LOG.info('Create import zone "A" using primary client')
zone_import = self.client.create_zone_import()[1]
self.addCleanup(self.clean_up_resources, zone_import['id'])
+ # Make sure we complete the import and have the zone_id for cleanup
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import['id'], const.COMPLETE)
LOG.info('As Alt user list import zones for a Primary tenant, '
'using "x-auth-sudo-project-id" HTTP header. '
@@ -189,8 +199,12 @@
headers={'x-auth-all-projects': True}))
LOG.info('As Admin tenant list import zones for all projects')
+ # Note: This is an all-projects list call, so other tests running
+ # in parallel will impact the list result set. Since the default
+ # pagination limit is only 20, we set a param limit of 1000 here.
body = self.admin_client.list_zone_imports(headers={
- 'x-auth-all-projects': True})[1]['imports']
+ 'x-auth-all-projects': True},
+ params={'limit': 1000})[1]['imports']
LOG.info('Ensure the fetched response includes previously '
'created import ID')
diff --git a/designate_tempest_plugin/tests/base.py b/designate_tempest_plugin/tests/base.py
index 6d33d81..a9a5e03 100644
--- a/designate_tempest_plugin/tests/base.py
+++ b/designate_tempest_plugin/tests/base.py
@@ -98,14 +98,22 @@
with context:
callable_(*args, **kwargs)
+ def transfer_request_delete(self, transfer_client, transfer_request_id):
+ return utils.call_and_ignore_notfound_exc(
+ transfer_client.delete_transfer_request, transfer_request_id)
+
def wait_zone_delete(self, zone_client, zone_id, **kwargs):
- zone_client.delete_zone(zone_id, **kwargs)
+ self._delete_zone(zone_client, zone_id, **kwargs)
utils.call_until_true(self._check_zone_deleted,
CONF.dns.build_timeout,
CONF.dns.build_interval,
zone_client,
zone_id)
+ def _delete_zone(self, zone_client, zone_id, **kwargs):
+ return utils.call_and_ignore_notfound_exc(zone_client.delete_zone,
+ zone_id, **kwargs)
+
def _check_zone_deleted(self, zone_client, zone_id):
return utils.call_and_ignore_notfound_exc(zone_client.show_zone,
zone_id) is None
diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 0000000..710f18d
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,9 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+sphinx>=2.0.0,!=2.1.0 # BSD
+openstackdocstheme>=2.2.1 # Apache-2.0
+
+# releasenotes
+reno>=3.1.0 # Apache-2.0
diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/releasenotes/source/_static/.placeholder
diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/releasenotes/source/_templates/.placeholder
diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py
new file mode 100644
index 0000000..7058407
--- /dev/null
+++ b/releasenotes/source/conf.py
@@ -0,0 +1,268 @@
+# 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.
+
+# Designate Release Notes documentation build configuration file, created by
+# sphinx-quickstart on Tue Nov 3 17:40:50 2015.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'openstackdocstheme',
+ 'reno.sphinxext',
+]
+
+# openstackdocstheme options
+openstackdocs_repo_name = 'openstack/designate-tempest-plugin'
+openstackdocs_bug_project = 'designate'
+openstackdocs_bug_tag = ''
+openstackdocs_auto_name = False
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+copyright = u'2021, Designate Developers'
+
+# Release notes are versions independent.
+# The short X.Y version.
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ''
+# Else, today_fmt is used as the format for a strftime call.
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'native'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'openstackdocs'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+# html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+# html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+# html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+# html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+# html_domain_indices = True
+
+# If false, no index is generated.
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'DesignateTempestPluginReleaseNotesdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'DesignateTempestPluginReleaseNotes.tex',
+ u'Designate Tempest Plugin Release Notes '
+ u'Documentation',
+ u'Designate Tempest Plugin Developers', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+# latex_appendices = []
+
+# If false, no module index is generated.
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'DesignateTempestPluginreleasenotes',
+ u'Designate Tempest Plugin Release Notes '
+ u'Documentation',
+ [u'Designate Tempest Plugin Developers'], 1)
+]
+
+# If true, show URL addresses after external links.
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'DesignateTempestPluginReleaseNotes',
+ u'Designate Tempest Plugin Release Notes '
+ u'Documentation',
+ u'Designate Tempest Plugin Developers',
+ 'DesignateTempestPluginReleaseNotes',
+ 'The tempest plugin for the Designate test suite.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+# texinfo_no_detailmenu = False
+
+# -- Options for Internationalization output ------------------------------
+locale_dirs = ['locale/']
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
new file mode 100644
index 0000000..cf4c77e
--- /dev/null
+++ b/releasenotes/source/index.rst
@@ -0,0 +1,18 @@
+..
+ 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.
+
+======================================
+Designate Tempest Plugin Release Notes
+======================================
+
+.. release-notes::
diff --git a/test-requirements.txt b/test-requirements.txt
index df0e096..c0a7610 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,5 +4,3 @@
# Hacking already pins down pep8/pycodestyle pyflakes and flake8
hacking>=3.0.1,<3.1.0 # Apache-2.0
-openstackdocstheme>=2.2.1 # Apache-2.0
-sphinx>=2.0.0,!=2.1.0 # BSD
diff --git a/tox.ini b/tox.ini
index b7f3098..f1581bc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -35,11 +35,19 @@
[testenv:pep8]
commands = sh tools/pretty_flake8.sh
-
[testenv:docs]
+deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
+ -r{toxinidir}/doc/requirements.txt
commands = rm -rf doc/build
sphinx-build -E -W -b html doc/source doc/build/html
+[testenv:releasenotes]
+deps = {[testenv:docs]deps}
+whitelist_externals = rm
+commands =
+ rm -rf releasenotes/build
+ sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
+
[testenv:bashate]
deps = bashate
whitelist_externals = bash