Merge "Updating python testing classifier as per Yoga testing runtime"
diff --git a/.zuul.yaml b/.zuul.yaml
index cf8323b..bea1b4c 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,22 +1,20 @@
- job:
- name: designate-bind9-stable-xena
+ name: designate-bind9-stable-2024-1
parent: designate-bind9
- override-checkout: stable/xena
+ nodeset: openstack-single-node-jammy
+ override-checkout: stable/2024.1
- job:
- name: designate-bind9-stable-wallaby
+ name: designate-bind9-stable-bobcat
parent: designate-bind9
- override-checkout: stable/wallaby
+ nodeset: openstack-single-node-jammy
+ override-checkout: stable/2023.2
- job:
- name: designate-bind9-stable-victoria
+ name: designate-bind9-stable-antelope
parent: designate-bind9
- override-checkout: stable/victoria
-
-- job:
- name: designate-bind9-stable-ussuri
- parent: designate-bind9
- override-checkout: stable/ussuri
+ nodeset: openstack-single-node-focal
+ override-checkout: stable/2023.1
- project:
templates:
@@ -27,10 +25,7 @@
- release-notes-jobs-python3
check:
jobs:
- - designate-bind9-stable-xena
- - designate-bind9-stable-wallaby
- - designate-bind9-stable-victoria
- - designate-bind9-stable-ussuri
+ - designate-bind9-stable-2024-1
+ - designate-bind9-stable-bobcat
+ - designate-bind9-stable-antelope
- neutron-tempest-plugin-designate-scenario
- gate:
- queue: designate
diff --git a/README.rst b/README.rst
index e1a2d04..902b26d 100644
--- a/README.rst
+++ b/README.rst
@@ -23,15 +23,25 @@
Running the tests
-----------------
+From the tempest directory, setup the tempest virtual environment for the
+designate tempest plugin::
+
+ $ tox -e venv-tempest -- pip3 install -e <path to designate-tempest-plugin>
+
+For example, when using a typical devstack setup::
+
+ $ cd /opt/stack/tempest
+ $ tox -e venv-tempest -- pip3 install -e /opt/stack/designate-tempest-plugin
+
To run all tests from this plugin, install designate into your environment
and from the tempest repo, run::
- $ tox -e all-plugin -- designate
+ $ tox -e all -- designate
To run a single test case, run with the test case name, for example::
- $ tox -e all-plugin -- designate_tempest_plugin.tests.api.v2.test_zones.ZonesAdminTest.test_get_other_tenant_zone
+ $ tox -e all -- designate_tempest_plugin.tests.api.v2.test_zones.ZonesTest.test_create_zones
To run all tempest tests including this plugin, run::
- $ tox -e all-plugin
+ $ tox -e all
diff --git a/designate_tempest_plugin/clients.py b/designate_tempest_plugin/clients.py
index c6eed6f..012f8a3 100644
--- a/designate_tempest_plugin/clients.py
+++ b/designate_tempest_plugin/clients.py
@@ -15,16 +15,16 @@
from tempest import config
from tempest.lib import auth
-from designate_tempest_plugin.services.dns.v2.json.blacklists_client import \
- BlacklistsClient
-from designate_tempest_plugin.services.dns.v2.json.pool_client import \
- PoolClient
-from designate_tempest_plugin.services.dns.v2.json.recordset_client import \
- RecordsetClient
-from designate_tempest_plugin.services.dns.v2.json.tld_client import \
- TldClient
-from designate_tempest_plugin.services.dns.v2.json.zones_client import \
- ZonesClient
+from designate_tempest_plugin.services.dns.v2.json.blacklists_client import (
+ BlacklistsClient)
+from designate_tempest_plugin.services.dns.v2.json.pool_client import (
+ PoolClient)
+from designate_tempest_plugin.services.dns.v2.json.recordset_client import (
+ RecordsetClient)
+from designate_tempest_plugin.services.dns.v2.json.tld_client import (
+ TldClient)
+from designate_tempest_plugin.services.dns.v2.json.zones_client import (
+ ZonesClient)
CONF = config.CONF
diff --git a/designate_tempest_plugin/common/constants.py b/designate_tempest_plugin/common/constants.py
index 4f6c72b..57aeab9 100644
--- a/designate_tempest_plugin/common/constants.py
+++ b/designate_tempest_plugin/common/constants.py
@@ -12,15 +12,31 @@
# License for the specific language governing permissions and limitations
# under the License.
-# Designate statuses strings
+# Designate statuses/actions strings
PENDING = 'PENDING'
COMPLETE = 'COMPLETE'
ERROR = 'ERROR'
DELETED = 'DELETED'
+DELETE = 'DELETE'
ACTIVE = 'ACTIVE'
+INACTIVE = 'INACTIVE'
UP = 'UP'
CREATE = 'CREATE'
+UPDATE = 'UPDATE'
+NONE = 'NONE'
# Zone types
PRIMARY_ZONE_TYPE = 'PRIMARY'
SECONDARY_ZONE_TYPE = 'SECONDARY'
+
+# DNS Record types
+A = 'A'
+AAAA = 'AAAA'
+ALIAS = 'ALIAS'
+CNAME = 'CNAME'
+MX = 'MX'
+NS = 'NS'
+PTR = 'PTR'
+SOA = 'SOA'
+SRV = 'SRV'
+TXT = 'TXT'
diff --git a/designate_tempest_plugin/common/exceptions.py b/designate_tempest_plugin/common/exceptions.py
index d3f343d..27bf848 100644
--- a/designate_tempest_plugin/common/exceptions.py
+++ b/designate_tempest_plugin/common/exceptions.py
@@ -15,7 +15,7 @@
class InvalidStatusError(Exception):
"""
- Exception raise when an entity changes to an unexpected status.
+ Exception raised when an entity changes to an unexpected status.
"""
def __init__(self, entity, entity_id, status, expected_status=None):
diff --git a/designate_tempest_plugin/common/waiters.py b/designate_tempest_plugin/common/waiters.py
index 6bc5dfa..cfa2d10 100644
--- a/designate_tempest_plugin/common/waiters.py
+++ b/designate_tempest_plugin/common/waiters.py
@@ -33,7 +33,7 @@
time.sleep(client.build_interval)
try:
- _, zone = client.show_zone(zone_id)
+ zone = client.show_zone(zone_id)[1]
except lib_exc.NotFound:
LOG.info('Zone %s is 404ing', zone_id)
return
@@ -62,17 +62,21 @@
"""Waits for a zone to reach given status."""
LOG.info('Waiting for zone %s to reach %s', zone_id, status)
- _, zone = client.show_zone(zone_id, headers=headers)
+ zone = client.show_zone(zone_id, headers=headers)[1]
start = int(time.time())
while zone['status'] != status:
time.sleep(client.build_interval)
- _, zone = client.show_zone(zone_id, headers=headers)
+ zone = client.show_zone(zone_id, headers=headers)[1]
status_curr = zone['status']
if status_curr == status:
LOG.info('Zone %s reached %s', zone_id, status)
return
+ if zone['status'] == const.ERROR:
+ raise exceptions.InvalidStatusError('Zone', zone_id,
+ zone['status'])
+
if int(time.time()) - start >= client.build_timeout:
message = ('Zone %(zone_id)s failed to reach status=%(status)s '
'within the required time (%(timeout)s s). Current '
@@ -94,17 +98,21 @@
"""Waits for an imported zone to reach the given status."""
LOG.info('Waiting for zone import %s to reach %s', zone_import_id, status)
- _, zone_import = client.show_zone_import(zone_import_id)
+ zone_import = client.show_zone_import(zone_import_id)[1]
start = int(time.time())
while zone_import['status'] != status:
time.sleep(client.build_interval)
- _, zone_import = client.show_zone_import(zone_import_id)
+ zone_import = client.show_zone_import(zone_import_id)[1]
status_curr = zone_import['status']
if status_curr == status:
LOG.info('Zone import %s reached %s', zone_import_id, status)
return
+ if zone_import['status'] == const.ERROR:
+ raise exceptions.InvalidStatusError('Zone Import', zone_import_id,
+ zone_import['status'])
+
if int(time.time()) - start >= client.build_timeout:
message = ('Zone import %(zone_import_id)s failed to reach '
'status=%(status)s within the required time '
@@ -123,21 +131,26 @@
raise lib_exc.TimeoutException(message)
-def wait_for_zone_export_status(client, zone_export_id, status):
+def wait_for_zone_export_status(client, zone_export_id, status, headers=None):
"""Waits for an exported zone to reach the given status."""
LOG.info('Waiting for zone export %s to reach %s', zone_export_id, status)
- _, zone_export = client.show_zone_export(zone_export_id)
+ zone_export = client.show_zone_export(zone_export_id, headers=headers)[1]
start = int(time.time())
while zone_export['status'] != status:
time.sleep(client.build_interval)
- _, zone_export = client.show_zone_export(zone_export_id)
+ zone_export = client.show_zone_export(
+ zone_export_id, headers=headers)[1]
status_curr = zone_export['status']
if status_curr == status:
LOG.info('Zone export %s reached %s', zone_export_id, status)
return
+ if zone_export['status'] == const.ERROR:
+ raise exceptions.InvalidStatusError('Zone Export', zone_export_id,
+ zone_export['status'])
+
if int(time.time()) - start >= client.build_timeout:
message = ('Zone export %(zone_export_id)s failed to reach '
'status=%(status)s within the required time '
@@ -156,22 +169,29 @@
raise lib_exc.TimeoutException(message)
-def wait_for_recordset_status(client, zone_id, recordset_id, status):
+def wait_for_recordset_status(
+ client, zone_id, recordset_id, status, headers=None):
"""Waits for a recordset to reach the given status."""
LOG.info('Waiting for recordset %s to reach %s',
recordset_id, status)
- _, recordset = client.show_recordset(zone_id, recordset_id)
+ recordset = client.show_recordset(
+ zone_id, recordset_id, headers=headers)[1]
start = int(time.time())
while recordset['status'] != status:
time.sleep(client.build_interval)
- _, recordset = client.show_recordset(zone_id, recordset_id)
+ recordset = client.show_recordset(
+ zone_id, recordset_id, headers=headers)[1]
status_curr = recordset['status']
if status_curr == status:
LOG.info('Recordset %s reached %s', recordset_id, status)
return
+ if recordset['status'] == const.ERROR:
+ raise exceptions.InvalidStatusError('Recordset', recordset_id,
+ recordset['status'])
+
if int(time.time()) - start >= client.build_timeout:
message = ('Recordset %(recordset_id)s failed to reach '
'status=%(status)s within the required time '
@@ -213,7 +233,7 @@
else:
all_answers_good = all(not r.answer for r in responses)
- if not client.nameservers or all_answers_good:
+ if all_answers_good:
LOG.info("Record %s of type %s was successfully %s on nameservers "
"%s", name, rdatatype, state, client.nameservers)
return
@@ -250,6 +270,10 @@
LOG.info('PTR %s reached %s', fip_id, status)
return
+ if ptr['status'] == const.ERROR:
+ raise exceptions.InvalidStatusError('PTR', fip_id,
+ ptr['status'])
+
if int(time.time()) - start >= client.build_timeout:
message = ('PTR for FIP: %(fip_id)s failed to reach '
'status=%(status)s within the required time '
diff --git a/designate_tempest_plugin/config.py b/designate_tempest_plugin/config.py
index 99c9a04..9b86945 100644
--- a/designate_tempest_plugin/config.py
+++ b/designate_tempest_plugin/config.py
@@ -41,18 +41,20 @@
default=360,
help="Timeout in seconds to wait for an resource to build."),
cfg.IntOpt('min_ttl',
- default=1,
- help="The minimum value to respect when generating ttls"),
+ default=0,
+ help="The minimum value to respect when generating ttl"),
cfg.ListOpt('nameservers',
default=[],
help="The nameservers to check for change going live"),
cfg.IntOpt('query_timeout',
- default=1,
+ default=3,
help="The timeout on a single dns query to a nameserver"),
cfg.StrOpt('zone_id',
help="The target zone to test the dns recordsets "
- "If it is not specified, a new zone will be created ")
-
+ "If it is not specified, a new zone will be created "),
+ cfg.StrOpt('tld_suffix',
+ default='test',
+ help="TLD suffix that used in all tests (if not overridden).")
]
dns_feature_group = cfg.OptGroup(name='dns_feature_enabled',
@@ -63,7 +65,7 @@
default=True,
help="Is the v2 dns API enabled."),
cfg.BoolOpt('api_admin',
- default=True,
+ default=False,
help="Is the admin dns API enabled."),
cfg.BoolOpt('api_v2_root_recordsets',
default=False,
@@ -77,8 +79,14 @@
"Must be set to True starting from Rocky release."),
cfg.BoolOpt('bug_1573141_fixed',
default=True,
+ deprecated_for_removal=True,
+ deprecated_reason='This bug was fixed in 3.0.0',
help="Is https://bugs.launchpad.net/designate/+bug/1573141 "
"fixed"),
+ cfg.BoolOpt('bug_1932026_fixed',
+ default=False,
+ help="Is https://bugs.launchpad.net/designate/+bug/1932026 "
+ "fixed"),
# Note: Also see the enforce_scope section (from tempest) for Designate API
# scope checking setting.
cfg.BoolOpt('enforce_new_defaults',
diff --git a/designate_tempest_plugin/data_utils.py b/designate_tempest_plugin/data_utils.py
index 4b5d24d..75a7d41 100644
--- a/designate_tempest_plugin/data_utils.py
+++ b/designate_tempest_plugin/data_utils.py
@@ -35,7 +35,7 @@
return an.format(netaddr.ipv6_compact)
-def rand_zone_name(name='', prefix='rand', suffix='.com.'):
+def rand_zone_name(name='', prefix='rand', suffix=None):
"""Generate a random zone name
:param str name: The name that you want to include
:param prefix: the exact text to start the string. Defaults to "rand"
@@ -43,6 +43,8 @@
:return: a random zone name e.g. example.org.
:rtype: string
"""
+ if suffix is None:
+ suffix = '.{}.'.format(CONF.dns.tld_suffix)
name = data_utils.rand_name(name=name, prefix=prefix)
return name + suffix
@@ -56,7 +58,7 @@
return 'example@%s' % domain.rstrip('.')
-def rand_ttl(start=1, end=86400):
+def rand_ttl(start=0, end=86400):
"""Generate a random TTL value
:return: a random ttl e.g. 165
:rtype: string
@@ -67,7 +69,6 @@
def rand_zonefile_data(name=None, ttl=None):
"""Generate random zone data, with optional overrides
-
:return: A ZoneModel
"""
zone_base = ('$ORIGIN &\n& # IN SOA ns.& nsadmin.& # # # # #\n'
@@ -94,8 +95,8 @@
}
if CONF.dns_feature_enabled.bug_1573141_fixed:
- quotas_dict['api_export_size'] = \
- api_export_size or data_utils.rand_int_id(100, 999999)
+ quotas_dict['api_export_size'] = (
+ api_export_size or data_utils.rand_int_id(100, 999999))
else:
LOG.warning("Leaving `api_export_size` out of quota data due to: "
"https://bugs.launchpad.net/designate/+bug/1573141")
@@ -105,11 +106,11 @@
def rand_zone_data(name=None, email=None, ttl=None, description=None):
"""Generate random zone data, with optional overrides
-
:return: A ZoneModel
"""
if name is None:
- name = rand_zone_name(prefix='testdomain', suffix='.com.')
+ name = rand_zone_name(
+ prefix='testdomain', suffix='.{}.'.format(CONF.dns.tld_suffix))
if email is None:
email = ("admin@" + name).strip('.')
if description is None:
@@ -124,15 +125,16 @@
def rand_recordset_data(record_type, zone_name, name=None, records=None,
- ttl=None):
+ ttl=None, number_of_records=None):
"""Generate random recordset data, with optional overrides
-
:return: A RecordsetModel
"""
if name is None:
name = rand_zone_name(prefix=record_type, suffix='.' + zone_name)
if records is None:
records = [rand_ip()]
+ if number_of_records:
+ records = [rand_ip() for r in range(number_of_records)]
if ttl is None:
ttl = rand_ttl()
return {
@@ -188,8 +190,7 @@
**kwargs):
algorithm_number = algorithm_number or 2
fingerprint_type = fingerprint_type or 1
- fingerprint = fingerprint or \
- "123456789abcdef67890123456789abcdef67890"
+ fingerprint = fingerprint or "123456789abcdef67890123456789abcdef67890"
data = "%s %s %s" % (algorithm_number, fingerprint_type, fingerprint)
return rand_recordset_data('SSHFP', zone_name, records=[data], **kwargs)
@@ -202,20 +203,34 @@
def wildcard_ns_recordset(zone_name):
name = "*.{0}".format(zone_name)
- records = ["ns.example.com."]
+ records = ["ns.example.{}.".format(CONF.dns.tld_suffix)]
return rand_recordset_data('NS', zone_name, name, records)
def rand_ns_records():
ns_zone = rand_zone_name()
- records = []
+ ns_records = []
+ # Make sure we don't have equal priority here which causes test failures
+ # when doing sorted comparisons
for i in range(0, 2):
- records.append("ns%s.%s" % (i, ns_zone))
- ns_records = [{"hostname": x, "priority": random.randint(1, 999)}
- for x in records]
+ ns_records.append({"hostname": "ns%s.%s" % (i, ns_zone),
+ "priority": (random.randint(1, 999) + i)})
return ns_records
+def rand_soa_records(number_of_records=2):
+ return ['{} {} {} {} {} {}.'.format(
+ '{}.{}.{}'.format(rand_string(3), rand_string(7), rand_string(3)),
+ random.randint(1000000000, 2020080302), random.randint(3000, 7200),
+ random.randint(1000, 3600), random.randint(1000000, 1209600),
+ random.randint(1000, 3600)) for i in range(0, number_of_records)]
+
+
+def rand_soa_recordset(zone_name, **kwargs):
+ return rand_recordset_data(
+ 'SOA', zone_name, records=rand_soa_records(), **kwargs)
+
+
def rand_tld():
data = {
"name": rand_zone_name(prefix='tld', suffix='')
@@ -225,7 +240,6 @@
def rand_transfer_request_data(description=None, target_project_id=None):
"""Generate random transfer request data, with optional overrides
-
:return: A TransferRequest data
"""
@@ -255,7 +269,6 @@
"""Create a rand recordset by type
This essentially just dispatches to the relevant random recordset
creation functions.
-
:param str zone_name: The zone name the recordset applies to
:param str record_type: The type of recordset (ie A, MX, NS, etc...)
"""
@@ -266,7 +279,6 @@
def rand_string(size):
"""Create random string of ASCII chars by size
-
:param int size - length os the string to be create
:return - random creates string of ASCII lover characters
"""
@@ -275,7 +287,6 @@
def rand_domain_name(tld=None):
"""Create random valid domain name
-
:param tld (optional) - TLD that will be used to random domain name
:return - valid domain name, for example: paka.zbabun.iuh
"""
diff --git a/designate_tempest_plugin/hacking/__init__.py b/designate_tempest_plugin/hacking/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/designate_tempest_plugin/hacking/__init__.py
diff --git a/designate_tempest_plugin/hacking/checks.py b/designate_tempest_plugin/hacking/checks.py
new file mode 100644
index 0000000..53de95c
--- /dev/null
+++ b/designate_tempest_plugin/hacking/checks.py
@@ -0,0 +1,187 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2012, Cloudscaling
+#
+# 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 re
+
+from hacking import core
+import pycodestyle
+
+# D701: Default parameter value is a mutable type
+# D702: Log messages require translation
+# D703: Found use of _() without explicit import of _!
+# D704: Found import of %s. This oslo library has been graduated!
+# D705: timeutils.utcnow() must be used instead of datetime.%s()
+# D706: Don't translate debug level logs
+# D707: basestring is not Python3-compatible, use str instead.
+# D708: Do not use xrange. Use range for large loops.
+# D709: LOG.audit is deprecated, please use LOG.info!
+# D710: LOG.warn() is not allowed. Use LOG.warning()
+# D711: Don't use backslashes for line continuation.
+
+UNDERSCORE_IMPORT_FILES = []
+
+
+mutable_default_argument_check = re.compile(
+ r"^\s*def .+\((.+=\{\}|.+=\[\])")
+string_translation = re.compile(r"[^_]*_\(\s*('|\")")
+translated_log = re.compile(
+ r"(.)*LOG\.(audit|error|info|warn|warning|critical|exception)"
+ r"\(\s*_\(\s*('|\")")
+underscore_import_check = re.compile(r"(.)*import _(.)*")
+# We need this for cases where they have created their own _ function.
+custom_underscore_check = re.compile(r"(.)*_\s*=\s*(.)*")
+graduated_oslo_libraries_import_re = re.compile(
+ r"^\s*(?:import|from) designate\.openstack\.common\.?.*?"
+ r"(gettextutils|rpc)"
+ r".*?")
+no_line_continuation_backslash_re = re.compile(r'.*(\\)\n')
+
+
+@core.flake8ext
+def mutable_default_arguments(physical_line, logical_line, filename):
+ if pycodestyle.noqa(physical_line):
+ return
+
+ if mutable_default_argument_check.match(logical_line):
+ yield (0, "D701: Default parameter value is a mutable type")
+
+
+@core.flake8ext
+def no_translate_debug_logs(logical_line, filename):
+ """Check for 'LOG.debug(_('
+ As per our translation policy,
+ https://wiki.openstack.org/wiki/LoggingStandards#Log_Translation
+ we shouldn't translate debug level logs.
+ * This check assumes that 'LOG' is a logger.
+ * Use filename so we can start enforcing this in specific folders instead
+ of needing to do so all at once.
+ N319
+ """
+ if logical_line.startswith("LOG.debug(_("):
+ yield(0, "D706: Don't translate debug level logs")
+
+
+@core.flake8ext
+def check_explicit_underscore_import(logical_line, filename):
+ """Check for explicit import of the _ function
+
+ We need to ensure that any files that are using the _() function
+ to translate logs are explicitly importing the _ function. We
+ can't trust unit test to catch whether the import has been
+ added so we need to check for it here.
+ """
+ # Build a list of the files that have _ imported. No further
+ # checking needed once it is found.
+ if filename in UNDERSCORE_IMPORT_FILES:
+ pass
+ elif (underscore_import_check.match(logical_line) or
+ custom_underscore_check.match(logical_line)):
+ UNDERSCORE_IMPORT_FILES.append(filename)
+ elif (translated_log.match(logical_line) or
+ string_translation.match(logical_line)):
+ yield(0, "D703: Found use of _() without explicit import of _!")
+
+
+@core.flake8ext
+def no_import_graduated_oslo_libraries(logical_line, filename):
+ """Check that we don't continue to use o.c. oslo libraries after graduation
+
+ After a library graduates from oslo-incubator, as we make the switch, we
+ should ensure we don't continue to use the oslo-incubator versions.
+
+ In many cases, it's not possible to immediately remove the code from the
+ openstack/common folder due to dependency issues.
+ """
+ # We can't modify oslo-incubator code, so ignore it here.
+ if "designate/openstack/common" in filename:
+ return
+
+ matches = graduated_oslo_libraries_import_re.match(logical_line)
+ if matches:
+ yield(0, "D704: Found import of %s. This oslo library has been "
+ "graduated!" % matches.group(1))
+
+
+@core.flake8ext
+def use_timeutils_utcnow(logical_line, filename):
+ # tools are OK to use the standard datetime module
+ if "/tools/" in filename:
+ return
+
+ msg = "D705: timeutils.utcnow() must be used instead of datetime.%s()"
+
+ datetime_funcs = ['now', 'utcnow']
+ for f in datetime_funcs:
+ pos = logical_line.find('datetime.%s' % f)
+ if pos != -1:
+ yield (pos, msg % f)
+
+
+@core.flake8ext
+def check_no_basestring(logical_line):
+ if re.search(r"\bbasestring\b", logical_line):
+ msg = ("D707: basestring is not Python3-compatible, use "
+ "str instead.")
+ yield(0, msg)
+
+
+@core.flake8ext
+def check_python3_xrange(logical_line):
+ if re.search(r"\bxrange\s*\(", logical_line):
+ yield(0, "D708: Do not use xrange. Use range for "
+ "large loops.")
+
+
+@core.flake8ext
+def check_no_log_audit(logical_line):
+ """Ensure that we are not using LOG.audit messages
+ Plans are in place going forward as discussed in the following
+ spec (https://review.opendev.org/#/c/132552/) to take out
+ LOG.audit messages. Given that audit was a concept invented
+ for OpenStack we can enforce not using it.
+ """
+ if "LOG.audit(" in logical_line:
+ yield(0, "D709: LOG.audit is deprecated, please use LOG.info!")
+
+
+@core.flake8ext
+def check_no_log_warn(logical_line):
+ """Disallow 'LOG.warn('
+
+ D710
+ """
+ if logical_line.startswith('LOG.warn('):
+ yield(0, "D710:Use LOG.warning() rather than LOG.warn()")
+
+
+@core.flake8ext
+def check_line_continuation_no_backslash(logical_line, tokens):
+ """D711 - Don't use backslashes for line continuation.
+
+ :param logical_line: The logical line to check. Not actually used.
+ :param tokens: List of tokens to check.
+ :returns: None if the tokens don't contain any issues, otherwise a tuple
+ is yielded that contains the offending index in the logical
+ line and a message describe the check validation failure.
+ """
+ backslash = None
+ for token_type, text, start, end, orig_line in tokens:
+ m = no_line_continuation_backslash_re.match(orig_line)
+ if m:
+ backslash = (start[0], m.start(1))
+ break
+
+ if backslash is not None:
+ msg = 'D711 Backslash line continuations not allowed'
+ yield backslash, msg
diff --git a/designate_tempest_plugin/services/dns/admin/__init__.py b/designate_tempest_plugin/services/dns/admin/__init__.py
index 2b43e84..a3b6ac0 100644
--- a/designate_tempest_plugin/services/dns/admin/__init__.py
+++ b/designate_tempest_plugin/services/dns/admin/__init__.py
@@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
-from designate_tempest_plugin.services.dns.admin.json.quotas_client import \
- QuotasClient
+from designate_tempest_plugin.services.dns.admin.json.quotas_client import (
+ QuotasClient)
__all__ = ['QuotasClient']
diff --git a/designate_tempest_plugin/services/dns/json/base.py b/designate_tempest_plugin/services/dns/json/base.py
index d484ac9..dc3eb6f 100644
--- a/designate_tempest_plugin/services/dns/json/base.py
+++ b/designate_tempest_plugin/services/dns/json/base.py
@@ -17,8 +17,7 @@
from oslo_serialization import jsonutils as json
from tempest.lib.common import rest_client
from tempest.lib import exceptions as lib_exc
-from six.moves.urllib import parse as urllib
-import six
+from urllib import parse as urllib_parse
from designate_tempest_plugin.common import models
@@ -58,7 +57,7 @@
DELETE_STATUS_CODES = []
def serialize(self, data):
- if isinstance(data, six.string_types):
+ if isinstance(data, str):
return data
return json.dumps(data)
@@ -106,7 +105,7 @@
else:
uuid = '/%s' % uuid if uuid else ''
- params = '?%s' % urllib.urlencode(params) if params else ''
+ params = '?%s' % urllib_parse.urlencode(params) if params else ''
return uri_pattern.format(pref=self.uri_prefix,
res=resource_name,
@@ -191,20 +190,29 @@
return resp, self.deserialize(resp, body)
- def _put_request(self, resource, uuid, data, params=None):
+ def _put_request(self, resource, uuid, data, params=None,
+ headers=None, extra_headers=False):
"""Updates the specified object using PUT request.
:param resource: The name of the REST resource, e.g., 'zones'.
:param uuid: Unique identifier of the object in UUID format.
:param data: A Python dict that represents an object of the
specified type (to be serialized) or a plain string which
is sent as-is.
+ :param headers (dict): The headers to use for the request.
: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.
+ :param extra_headers (bool): Boolean value than indicates if the
+ headers returned by the get_headers()
+ method are to be used but additional
+ headers are needed in the request
+ pass them in as a dict.
:returns: Serialized object as a dictionary.
"""
body = self.serialize(data)
uri = self.get_uri(resource, uuid=uuid, params=params)
- resp, body = self.put(uri, body=body)
+ resp, body = self.put(
+ uri, body=body, headers=headers, extra_headers=extra_headers)
self.expected_success(self.PUT_STATUS_CODES, resp.status)
@@ -268,3 +276,41 @@
body = self.deserialize(resp, body)
return resp, body
+
+ def get_max_api_version(self):
+ """Get the maximum version available on the API endpoint.
+ :return: Maximum version string available on the endpoint.
+ """
+ response, body = self.get('/')
+ self.expected_success(200, response.status)
+
+ versions_list = json.loads(body)['versions']
+
+ # Handle the legacy version document format
+ if 'values' in versions_list:
+ versions_list = versions_list['values']
+
+ current_versions = (version for version in versions_list if
+ version['status'] == 'CURRENT')
+ max_version = '0.0'
+ for version in current_versions:
+
+ ver_string = version['id']
+ if ver_string.startswith("v"):
+ ver_string = ver_string[1:]
+
+ ver_split = list(map(int, ver_string.split('.')))
+ max_split = list(map(int, max_version.split('.')))
+
+ if len(ver_split) > 2:
+ raise lib_exc.InvalidAPIVersionString(version=ver_string)
+
+ if ver_split[0] > max_split[0] or (
+ ver_split[0] == max_split[0] and
+ ver_split[1] >= max_split[1]):
+ max_version = ver_string
+
+ if max_version == '0.0':
+ raise lib_exc.InvalidAPIVersionString(version=max_version)
+
+ return max_version
diff --git a/designate_tempest_plugin/services/dns/query/query_client.py b/designate_tempest_plugin/services/dns/query/query_client.py
index da1d1b0..95fcb61 100644
--- a/designate_tempest_plugin/services/dns/query/query_client.py
+++ b/designate_tempest_plugin/services/dns/query/query_client.py
@@ -14,8 +14,8 @@
import dns
import dns.exception
import dns.query
-import six
from tempest import config
+from oslo_utils import netutils
CONF = config.CONF
@@ -25,15 +25,17 @@
def __init__(self, nameservers=None, query_timeout=None,
build_interval=None, build_timeout=None):
- self.nameservers = nameservers or []
+ self.nameservers = nameservers or CONF.dns.nameservers
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):
+ if not self.nameservers:
+ raise ValueError('Nameservers list cannot be empty and it should '
+ 'contain DNS backend IPs to "dig" for')
return [c.query(zone_name, rdatatype) for c in self.clients]
@@ -51,7 +53,7 @@
@classmethod
def _prepare_query(cls, zone_name, rdatatype):
# support plain strings: "SOA", "A"
- if isinstance(rdatatype, six.string_types):
+ if isinstance(rdatatype, str):
rdatatype = dns.rdatatype.from_text(rdatatype)
dns_message = dns.message.make_query(zone_name, rdatatype)
dns_message.set_opcode(dns.opcode.QUERY)
@@ -77,7 +79,7 @@
@classmethod
def from_str(self, nameserver):
- if ':' in nameserver:
- ip, port = nameserver.rsplit(':', 1)
- return Nameserver(ip, int(port))
+ ip, port = netutils.parse_host_port(nameserver)
+ if port:
+ return Nameserver(ip, port)
return Nameserver(nameserver)
diff --git a/designate_tempest_plugin/services/dns/v2/__init__.py b/designate_tempest_plugin/services/dns/v2/__init__.py
index 44cc403..acc26d2 100644
--- a/designate_tempest_plugin/services/dns/v2/__init__.py
+++ b/designate_tempest_plugin/services/dns/v2/__init__.py
@@ -12,38 +12,25 @@
# License for the specific language governing permissions and limitations under
# the License.
-from designate_tempest_plugin.services.dns.v2.json.blacklists_client import \
- BlacklistsClient
-from designate_tempest_plugin.services.dns.v2.json.designate_limit_client \
- import DesignateLimitClient
-from designate_tempest_plugin.services.dns.v2.json.pool_client import \
- PoolClient
-from designate_tempest_plugin.services.dns.v2.json.ptr_client \
- import PtrClient
-from designate_tempest_plugin.services.dns.v2.json.quotas_client import \
- QuotasClient
-from designate_tempest_plugin.services.dns.v2.json.recordset_client import \
- RecordsetClient
-from designate_tempest_plugin.services.dns.v2.json.service_client import \
- ServiceClient
-from designate_tempest_plugin.services.dns.v2.json.tld_client import TldClient
-from designate_tempest_plugin.services.dns.v2.json.transfer_accepts_client \
- import TransferAcceptClient
-from designate_tempest_plugin.services.dns.v2.json.transfer_request_client \
- import TransferRequestClient
-from designate_tempest_plugin.services.dns.v2.json.tsigkey_client import \
- TsigkeyClient
-from designate_tempest_plugin.services.dns.v2.json.zones_client import \
- ZonesClient
-from designate_tempest_plugin.services.dns.v2.json.zone_exports_client import \
- ZoneExportsClient
-from designate_tempest_plugin.services.dns.v2.json.zone_imports_client import \
- ZoneImportsClient
-from designate_tempest_plugin.services.dns.v2.json.api_version_client import \
- ApiVersionClient
+from .json.blacklists_client import BlacklistsClient
+from .json.designate_limit_client import DesignateLimitClient
+from .json.pool_client import PoolClient
+from .json.ptr_client import PtrClient
+from .json.quotas_client import QuotasClient
+from .json.recordset_client import RecordsetClient
+from .json.service_client import ServiceClient
+from .json.shared_zones_client import SharedZonesClient
+from .json.tld_client import TldClient
+from .json.transfer_accepts_client import TransferAcceptClient
+from .json.transfer_request_client import TransferRequestClient
+from .json.tsigkey_client import TsigkeyClient
+from .json.zones_client import ZonesClient
+from .json.zone_exports_client import ZoneExportsClient
+from .json.zone_imports_client import ZoneImportsClient
+from .json.api_version_client import ApiVersionClient
__all__ = ['BlacklistsClient', 'DesignateLimitClient', 'PoolClient',
'PtrClient', 'QuotasClient', 'RecordsetClient', 'ServiceClient',
- 'TldClient', 'TransferAcceptClient', 'TransferRequestClient',
- 'TsigkeyClient', 'ZonesClient', 'ZoneExportsClient',
- 'ZoneImportsClient', 'ApiVersionClient']
+ 'SharedZonesClient', 'TldClient', 'TransferAcceptClient',
+ 'TransferRequestClient', 'TsigkeyClient', 'ZonesClient',
+ 'ZoneExportsClient', 'ZoneImportsClient', 'ApiVersionClient']
diff --git a/designate_tempest_plugin/services/dns/v2/json/quotas_client.py b/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
index 97398c0..1b1d005 100644
--- a/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/quotas_client.py
@@ -58,17 +58,29 @@
return resp, body
@base.handle_errors
- def show_quotas(self, project_id, params=None, headers=None):
+ def show_quotas(self, project_id=None, params=None, headers=None):
"""Gets a specific quota.
- :param project_id: Show the quotas of this project id
+ :param project_id: if provided - show the quotas of this project id.
+ https://docs.openstack.org/api-ref/dns/?expanded=
+ get-the-name-servers-for-a-zone-detail#view-quotas
+ If not - show the quotas for a current
+ project.
+ https://docs.openstack.org/api-ref/dns/?expanded=ge
+ t-the-name-servers-for-a-zone-detail#view-current-p
+ roject-s-quotas
+
: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: Serialized quota as a dictionary.
"""
- return self._show_request('quotas', project_id, params=params,
- headers=headers, extra_headers=True)
+ if project_id is None:
+ return self._show_request(
+ 'quotas', uuid=None, params=params, headers=headers)
+ else:
+ return self._show_request(
+ 'quotas', project_id, params=params, headers=headers)
@base.handle_errors
def delete_quotas(self, project_id, params=None, headers=None):
@@ -100,13 +112,10 @@
:param headers (dict): The headers to use for the request.
:return: Serialized quota as a dictionary.
"""
- if headers is None:
- headers = {'content-type': 'application/json'}
- if 'content-type' not in [header.lower() for header in headers]:
- headers['content-type'] = 'application/json'
resp, body = self._update_request(
"quotas", project_id,
- data=quotas, params=params, headers=headers)
+ data=quotas, params=params, headers=headers,
+ extra_headers=True)
self.expected_success(200, resp.status)
return resp, body
diff --git a/designate_tempest_plugin/services/dns/v2/json/recordset_client.py b/designate_tempest_plugin/services/dns/v2/json/recordset_client.py
index 33e9ee2..ed07817 100644
--- a/designate_tempest_plugin/services/dns/v2/json/recordset_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/recordset_client.py
@@ -35,7 +35,7 @@
@base.handle_errors
def create_recordset(self, zone_uuid, recordset_data,
- params=None, wait_until=False):
+ params=None, headers=None, wait_until=False):
"""Create a recordset for the specified zone.
:param zone_uuid: Unique identifier of the zone in UUID format..
@@ -43,23 +43,31 @@
data.
: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 created zone.
"""
- resp, body = self._create_request(
- "/zones/{0}/recordsets".format(zone_uuid), params=params,
- data=recordset_data)
+ if headers:
+ resp, body = self._create_request(
+ "/zones/{0}/recordsets".format(zone_uuid), params=params,
+ data=recordset_data, extra_headers=True, headers=headers)
+ else:
+ resp, body = self._create_request(
+ "/zones/{0}/recordsets".format(zone_uuid), params=params,
+ data=recordset_data)
# Create Recordset should Return a HTTP 202
self.expected_success(202, resp.status)
if wait_until:
- waiters.wait_for_recordset_status(self, body['id'], wait_until)
+ waiters.wait_for_recordset_status(
+ self, zone_uuid, body['id'], wait_until, headers=headers)
return resp, body
@base.handle_errors
def update_recordset(self, zone_uuid, recordset_uuid,
- recordet_data, params=None):
+ recordset_data, params=None,
+ headers=None, extra_headers=None, wait_until=False):
"""Update the recordset related to the specified zone.
:param zone_uuid: Unique identifier of the zone in UUID format.
:param recordset_uuid: Unique identifier of the recordset in UUID
@@ -68,16 +76,29 @@
data.
: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.
+ :param extra_headers (bool): Boolean value than indicates if the
+ headers returned by the get_headers()
+ method are to be used but additional
+ headers are needed in the request
+ pass them in as a dict.
+ :param wait_until: Block until the recordset reaches the
+ desired status
:return: A tuple with the server response and the created zone.
"""
resp, body = self._put_request(
'zones/{0}/recordsets'.format(zone_uuid), recordset_uuid,
- data=recordet_data, params=params)
+ data=recordset_data, params=params,
+ headers=headers, extra_headers=extra_headers)
# Update Recordset should Return a HTTP 202, or a 200 if the recordset
# is already active
self.expected_success([200, 202], resp.status)
+ if wait_until:
+ waiters.wait_for_recordset_status(
+ self, zone_uuid, body['id'], wait_until)
+
return resp, body
@base.handle_errors
@@ -97,17 +118,20 @@
params=params, headers=headers)
@base.handle_errors
- def delete_recordset(self, zone_uuid, recordset_uuid, params=None):
+ def delete_recordset(self, zone_uuid, recordset_uuid, params=None,
+ headers=None):
"""Deletes a recordset related to the specified zone UUID.
:param zone_uuid: The unique identifier of the zone.
:param recordset_uuid: The unique identifier of the record in
uuid format.
: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/{0}/recordsets'.format(zone_uuid), recordset_uuid)
+ 'zones/{0}/recordsets'.format(zone_uuid), recordset_uuid,
+ params=params, headers=headers)
# Delete Recordset should Return a HTTP 202
self.expected_success(202, resp.status)
diff --git a/designate_tempest_plugin/services/dns/v2/json/service_client.py b/designate_tempest_plugin/services/dns/v2/json/service_client.py
index 267e7a5..87da51a 100644
--- a/designate_tempest_plugin/services/dns/v2/json/service_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/service_client.py
@@ -25,3 +25,14 @@
"""
return self._list_request(
'service_statuses', headers=headers)[1]['service_statuses']
+
+ @base.handle_errors
+ def show_statuses(self, uuid, headers=None):
+ """Show Service status
+
+ :param headers: (dict): The headers to use for the request.
+ :param uuid: service ID
+ :return: Service status dictionary
+ """
+ return self._show_request(
+ 'service_statuses', uuid, headers=headers)[1]
diff --git a/designate_tempest_plugin/services/dns/v2/json/shared_zones_client.py b/designate_tempest_plugin/services/dns/v2/json/shared_zones_client.py
new file mode 100644
index 0000000..d908b38
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/v2/json/shared_zones_client.py
@@ -0,0 +1,80 @@
+# Copyright 2020 Cloudification GmbH. All rights reserved.
+#
+# 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 designate_tempest_plugin.services.dns.v2.json import base
+
+
+class SharedZonesClient(base.DnsClientV2Base):
+
+ @base.handle_errors
+ def create_zone_share(self, zone_id, target_project_id, headers=None):
+ """Create a new zone share for a project ID.
+
+ :param zone_id: Zone UUID to share
+ :param target_project_id: Project ID that will gain access to specified
+ zone
+ :param headers: (dict): The headers to use for the request.
+ :return: Zone share dict
+ """
+ resp, body = self._create_request(
+ 'zones/{}/shares'.format(zone_id),
+ data={'target_project_id': target_project_id}, headers=headers,
+ extra_headers=True)
+
+ # Endpoint should Return a HTTP 201
+ self.expected_success(201, resp.status)
+
+ return resp, body
+
+ @base.handle_errors
+ def show_zone_share(self, zone_id, zone_share_id, headers=None):
+ """Get the zone share object
+
+ :param zone_id: Zone UUID for the share
+ :param zone_share_id: The zone share ID
+ :param headers: (dict): The headers to use for the request.
+ :return: Zone share dict
+ """
+ return self._show_request('zones/{}/shares'.format(zone_id),
+ zone_share_id, headers=headers)
+
+ @base.handle_errors
+ def list_zone_shares(self, zone_id, params=None, headers=None):
+ """List zone shares
+
+ :param zone_id: Zone UUID to query for the shares
+ :param params: A Python dict that represents the query parameters to
+ include in the request URI.
+ :param headers: (dict): The headers to use for the request.
+ :return: Zone shares list.
+ """
+ return self._list_request('zones/{}/shares'.format(zone_id),
+ params=params, headers=headers)
+
+ @base.handle_errors
+ def delete_zone_share(self, zone_id, zone_share_id, headers=None):
+ """Deletes the zone share
+
+ :param zone_id: Zone UUID for the share
+ :param zone_share_id: The zone share ID
+ :param headers: (dict): The headers to use for the request.
+ :return: None
+ """
+ resp, body = self._delete_request('zones/{}/shares'.format(zone_id),
+ zone_share_id, headers=headers)
+
+ # Endpoint should Return a HTTP 204 - No Content
+ self.expected_success(204, 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 e09f775..9ee060d 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,20 +19,25 @@
@base.handle_errors
def create_transfer_accept(self, transfer_accept_data,
- params=None, headers=None):
+ params=None, headers=None, extra_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.
+ :param extra_headers (bool): Boolean value than indicates if the
+ headers returned by the get_headers()
+ method are to be used but additional
+ headers are needed in the request
+ pass them in as a dict.
: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, headers=headers)
+ params=params, headers=headers, extra_headers=extra_headers)
# Create Transfer accept should Return a HTTP 201
self.expected_success(201, resp.status)
@@ -61,5 +66,4 @@
:return: List of accepted zone transfers
"""
return self._list_request(
- 'zones/tasks/transfer_accepts', params=params,
- headers=headers)[1]['transfer_accepts']
+ 'zones/tasks/transfer_accepts', params=params, headers=headers)
diff --git a/designate_tempest_plugin/services/dns/v2/json/transfer_request_client.py b/designate_tempest_plugin/services/dns/v2/json/transfer_request_client.py
index 9523175..e2d35e2 100644
--- a/designate_tempest_plugin/services/dns/v2/json/transfer_request_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/transfer_request_client.py
@@ -103,7 +103,7 @@
@base.handle_errors
def update_transfer_request(self, uuid, transfer_request_data=None,
- params=None):
+ params=None, headers=None):
"""Update a zone transfer_requests.
:param uuid: Unique identifier of the zone transfer request in UUID
format.
@@ -111,13 +111,15 @@
data for zone transfer request
: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: Serialized imported zone as a dictionary.
"""
transfer_request_uri = 'zones/tasks/transfer_requests'
transfer_request_data = (transfer_request_data or
dns_data_utils.rand_transfer_request_data())
resp, body = self._update_request(
- transfer_request_uri, uuid, transfer_request_data, params=params)
+ transfer_request_uri, uuid, transfer_request_data, params=params,
+ headers=headers)
# Create Transfer request should Return a HTTP 200
self.expected_success(200, resp.status)
diff --git a/designate_tempest_plugin/services/dns/v2/json/tsigkey_client.py b/designate_tempest_plugin/services/dns/v2/json/tsigkey_client.py
index 683c1bb..61c632a 100644
--- a/designate_tempest_plugin/services/dns/v2/json/tsigkey_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/tsigkey_client.py
@@ -28,8 +28,9 @@
:param resource_id: Pool id or Zone id.
:param name: name of the tsigkey.
:param algorithm: TSIG algorithm e.g hmac-md5, hmac-sha256 etc.
- :param secret: represents TSIG secret.
- :param scope: represents TSIG scope.
+ :param secret: represents TSIG secret. If provided value is empty
+ it will use empty string (Needed for negative testing)
+ :param scope: represents TSIG scope. Default is ZONE
:param params: A Python dict that represents the query paramaters to
include in the request URI.
:return: A tuple with the server response and the created tsigkey.
@@ -38,9 +39,12 @@
"name": name or data_utils.rand_name('test-tsig'),
"algorithm": algorithm or utils.rand_tsig_algorithm(),
"secret": secret or data_utils.rand_name("secret"),
- "scope": scope or utils.rand_tsig_scope(),
+ "scope": scope or 'ZONE',
"resource_id": resource_id}
+ if secret == '':
+ tsig['secret'] = ''
+
resp, body = self._create_request('tsigkeys', data=tsig,
params=params)
@@ -49,23 +53,28 @@
return resp, body
@base.handle_errors
- def list_tsigkeys(self, params=None):
+ def list_tsigkeys(self, params=None, headers=None):
"""Gets a list of tsigkeys.
: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: Serialized tsigkeys as a list.
"""
- return self._list_request('tsigkeys', params=params)
+ return self._list_request('tsigkeys', params=params, headers=headers)
@base.handle_errors
- def show_tsigkey(self, uuid, params=None):
+ def show_tsigkey(self, uuid=None, params=None, headers=None):
"""Gets a specific tsigkey.
:param uuid: Unique identifier of the tsigkey in UUID format.
+ Default value is None (it's possible to use "marker" in
+ URL query instead of UUID)
: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: Serialized tsigkey as a dictionary.
"""
- return self._show_request('tsigkeys', uuid, params=params)
+ return self._show_request(
+ 'tsigkeys', uuid, params=params, headers=headers)
@base.handle_errors
def update_tsigkey(self, uuid, name=None, algorithm=None,
diff --git a/designate_tempest_plugin/services/dns/v2/json/zone_exports_client.py b/designate_tempest_plugin/services/dns/v2/json/zone_exports_client.py
index 8915ceb..5089d36 100644
--- a/designate_tempest_plugin/services/dns/v2/json/zone_exports_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/zone_exports_client.py
@@ -19,26 +19,29 @@
class ZoneExportsClient(base.DnsClientV2Base):
@base.handle_errors
- def create_zone_export(self, uuid, params=None, wait_until=False):
+ def create_zone_export(self, uuid, params=None,
+ wait_until=False, headers=None):
"""Create a zone export.
:param uuid: Unique identifier of the zone in UUID format.
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
:param wait_until: Block until the exported zone reaches the
desired status
+ :param headers (dict): The headers to use for the request.
:return: Serialized imported zone as a dictionary.
"""
export_uri = 'zones/{0}/tasks/export'.format(uuid)
resp, body = self._create_request(
- export_uri, params=params)
+ export_uri, params=params, headers=headers)
# Create Zone Export should Return a HTTP 202
self.expected_success(202, resp.status)
if wait_until:
- waiters.wait_for_zone_export_status(self, body['id'], wait_until)
+ waiters.wait_for_zone_export_status(
+ self, body['id'], wait_until, headers=headers)
return resp, body
@@ -47,7 +50,7 @@
"""Get the zone export task
:param uuid: Unique identifier of the zone export task in UUID format.
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
:param headers (dict): The headers to use for the request.
:return: Serialized exported zone as a dictionary.
@@ -56,25 +59,42 @@
'zones/tasks/exports', uuid, params=params, headers=headers)
@base.handle_errors
- def show_exported_zonefile(self, uuid, params=None):
+ def show_exported_zonefile(self, uuid, params=None, headers=None):
+
"""Get the exported zone file
- :param uuid: Unique identifier of the zone exprot task in UUID format.
- :param params: A Python dict that represents the query paramaters to
+ :param uuid: Unique identifier of the zone export task in UUID format.
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
+ :param headers: 3 options to send headers:
+ 1) If headers dict provided is missing "Accept" key -
+ "{Accept:text/dns}" will be added.
+ 2) If header is None -
+ "{Accept:text/dns}" will be sent.
+ 3) If function is called with no headers,
+ means empty dict {} -
+ no headers will be sent (empty dictionary)
+
:return: Serialized exported zone as a dictionary.
"""
- headers = {'Accept': 'text/dns'}
+
+ if headers:
+ if 'accept' not in [key.lower() for key in headers.keys()]:
+ headers['Accept'] = 'text/dns'
+ elif headers is None:
+ headers = {'Accept': 'text/dns'}
+ else:
+ headers = {}
return self._show_request(
- 'zones/tasks/exports/{0}/export'.format(uuid), uuid='',
- headers=headers, params=params)
+ 'zones/tasks/exports/{0}/export'.format(uuid),
+ uuid='', headers=headers, params=params)
@base.handle_errors
def list_zone_exports(self, params=None, headers=None):
"""List zone export tasks
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
:param headers (dict): The headers to use for the request.
:return: Serialized exported zone as a list.
@@ -83,16 +103,17 @@
'zones/tasks/exports', params=params, headers=headers)
@base.handle_errors
- def delete_zone_export(self, uuid, params=None):
+ def delete_zone_export(self, uuid, params=None, headers=None):
"""Deletes the zone export task with the specified UUID.
:param uuid: The unique identifier of the exported zone.
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters 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/tasks/exports', uuid, params=params)
+ 'zones/tasks/exports', uuid, params=params, headers=headers)
# Delete Zone export should Return a HTTP 204
self.expected_success(204, resp.status)
diff --git a/designate_tempest_plugin/services/dns/v2/json/zone_imports_client.py b/designate_tempest_plugin/services/dns/v2/json/zone_imports_client.py
index 86d9fb1..236e737 100644
--- a/designate_tempest_plugin/services/dns/v2/json/zone_imports_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/zone_imports_client.py
@@ -21,15 +21,16 @@
@base.handle_errors
def create_zone_import(self, zonefile_data=None,
- params=None, wait_until=None):
+ wait_until=None, headers=None):
"""Create a zone import.
:param zonefile_data: A tuple that represents zone data.
- :param params: A Python dict that represents the query paramaters to
- include in the request URI.
+ :param wait_until: If not None, a waiter for appropriate status
+ will be activated.
+ :param headers (dict): The headers to use for the request.
:return: Serialized imported zone as a dictionary.
"""
-
- headers = {'Content-Type': 'text/dns'}
+ if not headers:
+ headers = {'Content-Type': 'text/dns'}
zone_data = zonefile_data or dns_data_utils.rand_zonefile_data()
resp, body = self._create_request(
'zones/tasks/imports', zone_data, headers=headers)
@@ -66,15 +67,16 @@
'zones/tasks/imports', params=params, headers=headers)
@base.handle_errors
- def delete_zone_import(self, uuid, params=None):
+ def delete_zone_import(self, uuid, params=None, headers=None):
"""Deletes a imported zone having the specified UUID.
:param uuid: The unique identifier of the imported 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/tasks/imports', uuid, params=params)
+ 'zones/tasks/imports', uuid, params=params, headers=headers)
# Delete Zone should Return a HTTP 204
self.expected_success(204, resp.status)
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 c30d24e..a995af8 100644
--- a/designate_tempest_plugin/services/dns/v2/json/zones_client.py
+++ b/designate_tempest_plugin/services/dns/v2/json/zones_client.py
@@ -48,7 +48,7 @@
Default: PRIMARY
:param primaries: List of Primary nameservers. Required for SECONDARY
Default: None
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
:param project_id: When specified, overrides the project ID the zone
will be associated with.
@@ -102,7 +102,7 @@
def show_zone(self, uuid, params=None, headers=None):
"""Gets a specific zone.
:param uuid: Unique identifier of the zone in UUID format.
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
:param headers (dict): The headers to use for the request.
:return: Serialized zone as a dictionary.
@@ -111,21 +111,22 @@
'zones', uuid, params=params, headers=headers)
@base.handle_errors
- def show_zone_nameservers(self, zone_uuid, params=None):
+ def show_zone_nameservers(self, zone_uuid, params=None, headers=None):
"""Gets list of Zone Name Servers
:param zone_uuid: Unique identifier of the zone in UUID format.
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
+ :param headers (dict): The headers to use for the request.
:return: Serialized nameservers as a list.
"""
return self._show_request(
'zones/{0}/nameservers'.format(zone_uuid), uuid=None,
- params=params)
+ params=params, headers=headers)
@base.handle_errors
def list_zones(self, params=None, headers=None):
"""Gets a list of zones.
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
:param headers (dict): The headers to use for the request.
:return: Serialized zones as a list.
@@ -133,14 +134,20 @@
return self._list_request('zones', params=params, headers=headers)
@base.handle_errors
- def delete_zone(self, uuid, params=None, headers=None):
+ def delete_zone(self, uuid, params=None, headers=None, delete_shares=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
+ :param params: A Python dict that represents the query parameters to
include in the request URI.
:param headers (dict): The headers to use for the request.
+ :param delete_shares: if set, delete-shares modifier will be activated
:return: A tuple with the server response and the response body.
"""
+ if delete_shares:
+ if headers:
+ headers['x-designate-delete-shares'] = True
+ else:
+ headers = {'x-designate-delete-shares': True}
resp, body = self._delete_request(
'zones', uuid, params=params, headers=headers)
@@ -151,7 +158,8 @@
@base.handle_errors
def update_zone(self, uuid, email=None, ttl=None,
- description=None, wait_until=False, params=None):
+ description=None, wait_until=False, params=None,
+ headers=None):
"""Update a zone with the specified parameters.
:param uuid: The unique identifier of the zone.
:param email: The email for the zone.
@@ -161,8 +169,9 @@
:param description: A description of the zone.
Default: Random Value
:param wait_until: Block until the zone reaches the desiered status
- :param params: A Python dict that represents the query paramaters to
+ :param params: A Python dict that represents the query parameters 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 updated zone.
"""
zone = {
@@ -171,7 +180,8 @@
'description': description or data_utils.rand_name('test-zone'),
}
- resp, body = self._update_request('zones', uuid, zone, params=params)
+ resp, body = self._update_request('zones', uuid, zone, params=params,
+ headers=headers)
# Update Zone should Return a HTTP 202
self.expected_success(202, resp.status)
diff --git a/designate_tempest_plugin/tests/api/v2/invalid_mx_dataset.json b/designate_tempest_plugin/tests/api/v2/invalid_mx_dataset.json
deleted file mode 100644
index 100995a..0000000
--- a/designate_tempest_plugin/tests/api/v2/invalid_mx_dataset.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "empty_preference": {"pref": ""},
- "minus_zero_preference": {"pref": "-0"},
- "minus_one_preference": {"pref": "-1"}
-}
diff --git a/designate_tempest_plugin/tests/api/v2/invalid_sshfp_dataset.json b/designate_tempest_plugin/tests/api/v2/invalid_sshfp_dataset.json
deleted file mode 100644
index 133b460..0000000
--- a/designate_tempest_plugin/tests/api/v2/invalid_sshfp_dataset.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "minus_zero_algorithm": {"algo": "-0", "finger": null},
- "minus_zero_fingerprint": {"algo": null, "finger": "-0"},
- "minus_one_algorithm": {"algo": "-1", "finger": null},
- "minus_one_fingerprint": {"algo": null, "finger": "-1"}
-}
diff --git a/designate_tempest_plugin/tests/api/v2/invalid_txt_dataset.json b/designate_tempest_plugin/tests/api/v2/invalid_txt_dataset.json
deleted file mode 100644
index 449d90c..0000000
--- a/designate_tempest_plugin/tests/api/v2/invalid_txt_dataset.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "trailing_slash": {"data": "\\"},
- "trailing_double_slash": {"data": "\\\\"},
- "trailing_slash_after_text": {"data": "testtext\\"}
-}
diff --git a/designate_tempest_plugin/tests/api/v2/recordset_data_invalid.json b/designate_tempest_plugin/tests/api/v2/recordset_data_invalid.json
deleted file mode 100644
index 413698f..0000000
--- a/designate_tempest_plugin/tests/api/v2/recordset_data_invalid.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "CNAME multiple": {
- "name": "www",
- "type": "CNAME",
- "records": ["target1.example.org.", "target2.example.org."]
- },
- "CNAME at Apex": {
- "name": null,
- "type": "CNAME",
- "records": ["target1.example.org."]
- }
-}
diff --git a/designate_tempest_plugin/tests/api/v2/recordset_wildcard_data.json b/designate_tempest_plugin/tests/api/v2/recordset_wildcard_data.json
deleted file mode 100644
index c56727d..0000000
--- a/designate_tempest_plugin/tests/api/v2/recordset_wildcard_data.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "A at APEX": {
- "name": "*",
- "type": "A",
- "records": ["192.0.2.1", "192.0.2.2", "192.0.2.3"]
- },
- "A under APEX": {
- "name": "*.sub",
- "type": "A",
- "records": ["192.0.2.1", "192.0.2.2", "192.0.2.3"]
- },
- "AAAA at APEX": {
- "name": "*",
- "type": "AAAA",
- "records": ["2001:db8::1", "2001:db8::1", "2001:db8::"]
- },
- "AAAA under APEX": {
- "name": "*.sub",
- "type": "AAAA",
- "records": ["2001:db8::1", "2001:db8::1", "2001:db8::"]
- },
- "MX at APEX": {
- "name": "*",
- "type": "MX",
- "records": ["10 mail1.example.org.",
- "20 mail2.example.org."]
- },
- "MX under APEX": {
- "name": "*.sub",
- "type": "MX",
- "records": ["10 mail.example.org."]
- },
- "SPF at APEX": {
- "name": "*",
- "type": "SPF",
- "records": ["\"v=spf1; a -all\""]
- },
- "SPF under APEX": {
- "name": "*.sub",
- "type": "SPF",
- "records": ["\"v=spf1; a -all\""]
- },
- "TXT at APEX": {
- "name": "*",
- "type": "TXT",
- "records": ["\"Can you read me?\""]
- },
- "TXT under APEX": {
- "name": "*.sub",
- "type": "TXT",
- "records": ["\"Can you read me?\""]
- }
-}
diff --git a/designate_tempest_plugin/tests/api/v2/test_blacklists.py b/designate_tempest_plugin/tests/api/v2/test_blacklists.py
index 95688b3..48b3028 100644
--- a/designate_tempest_plugin/tests/api/v2/test_blacklists.py
+++ b/designate_tempest_plugin/tests/api/v2/test_blacklists.py
@@ -30,7 +30,6 @@
class BlacklistsAdminTest(BaseBlacklistsTest):
- credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
# Do not create network resources for these test.
@@ -59,8 +58,12 @@
self.assertExpected(blacklist, body, self.excluded_keys)
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_CUD_RBAC_enforcement('BlacklistsClient', 'create_blacklist',
+ expected_allowed, False)
+
@decorators.idempotent_id('ea608152-da3c-11eb-b8b8-74e5f9e2a801')
- @decorators.skip_because(bug="1934252")
def test_create_blacklist_invalid_pattern(self):
patterns = ['', '#(*&^%$%$#@$', 'a' * 1000]
for pattern in patterns:
@@ -95,6 +98,15 @@
LOG.info('Ensure the fetched response matches the created blacklist')
self.assertExpected(blacklist, body, self.excluded_keys)
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin', 'os_system_reader']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'BlacklistsClient', 'show_blacklist', expected_allowed, False,
+ blacklist['id'])
+
@decorators.idempotent_id('dcea40d9-8d36-43cb-8440-4a842faaef0d')
def test_delete_blacklist(self):
LOG.info('Create a blacklist')
@@ -108,6 +120,12 @@
# A blacklist delete returns an empty body
self.assertEqual(body.strip(), b"")
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_CUD_RBAC_enforcement(
+ 'BlacklistsClient', 'delete_blacklist', expected_allowed, False,
+ blacklist['id'])
+
@decorators.idempotent_id('3a2a1e6c-8176-428c-b5dd-d85217c0209d')
def test_list_blacklists(self):
LOG.info('Create a blacklist')
@@ -120,6 +138,15 @@
# TODO(pglass): Assert that the created blacklist is in the response
self.assertGreater(len(body['blacklists']), 0)
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'BlacklistsClient', 'list_blacklists',
+ expected_allowed, [blacklist['id']])
+
@decorators.idempotent_id('0063d6ad-9557-49c7-b521-e64a14d4d0d0')
def test_update_blacklist(self):
LOG.info('Create a blacklist')
@@ -139,10 +166,16 @@
self.assertEqual(pattern, body['pattern'])
self.assertEqual(description, body['description'])
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_CUD_RBAC_enforcement(
+ 'BlacklistsClient', 'update_blacklist', expected_allowed, False,
+ uuid=blacklist['id'], pattern=pattern, description=description)
+
class TestBlacklistNotFoundAdmin(BaseBlacklistsTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
@@ -188,7 +221,7 @@
class TestBlacklistInvalidIdAdmin(BaseBlacklistsTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
diff --git a/designate_tempest_plugin/tests/api/v2/test_designate_limits.py b/designate_tempest_plugin/tests/api/v2/test_designate_limits.py
index 2cb9d9e..102f168 100644
--- a/designate_tempest_plugin/tests/api/v2/test_designate_limits.py
+++ b/designate_tempest_plugin/tests/api/v2/test_designate_limits.py
@@ -24,7 +24,8 @@
class DesignateLimit(base.BaseDnsV2Test):
- credentials = ["admin", "system_admin", "primary", "alt"]
+ credentials = ["admin", "system_admin", "system_reader", "primary", "alt",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -102,3 +103,14 @@
project_id, received_project_ids,
'Failed, expected project_id:{} is missing in:{} '.format(
project_id, received_project_ids))
+
+ @decorators.idempotent_id('fc57fa6b-5280-4186-9be9-ff4da0961db0')
+ def test_list_designate_limits_RBAC(self):
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_system_reader',
+ 'os_project_member', 'os_project_reader'])
+
+ self.check_list_show_RBAC_enforcement(
+ 'DesignateLimitClient', 'list_designate_limits',
+ expected_allowed, False)
diff --git a/designate_tempest_plugin/tests/api/v2/test_enabled_api_version.py b/designate_tempest_plugin/tests/api/v2/test_enabled_api_version.py
index c5fcd69..4eacfb9 100644
--- a/designate_tempest_plugin/tests/api/v2/test_enabled_api_version.py
+++ b/designate_tempest_plugin/tests/api/v2/test_enabled_api_version.py
@@ -45,27 +45,47 @@
def test_list_enabled_api_versions(self):
for user in ['admin', 'primary', 'not_auth_user']:
if user == 'admin':
- versions = self.admin_client.list_enabled_api_versions()[1][
- 'versions']['values']
+ ver_doc = self.admin_client.list_enabled_api_versions()[1]
+ # The version document was updated to match OpenStack
+ # version discovery standards in Zed. Accomodate the legacy
+ # format for backward compatibility.
+ try:
+ versions = ver_doc['versions']['values']
+ except TypeError:
+ versions = ver_doc['versions']
if user == 'primary':
- versions = self.primary_client.list_enabled_api_versions()[1][
- 'versions']['values']
+ ver_doc = self.primary_client.list_enabled_api_versions()[1]
+ # The version document was updated to match OpenStack
+ # version discovery standards in Zed. Accomodate the legacy
+ # format for backward compatibility.
+ try:
+ versions = ver_doc['versions']['values']
+ except TypeError:
+ versions = ver_doc['versions']
if user == 'not_auth_user':
response = requests.get(self.primary_client.base_url,
verify=False)
headers = {
k.lower(): v.lower() for k, v in response.headers.items()}
- versions = self.deserialize(
- headers, str(response.text))['versions']['values']
+ # The version document was updated to match OpenStack
+ # version discovery standards in Zed. Accomodate the legacy
+ # format for backward compatibility.
+ try:
+ versions = self.deserialize(
+ headers, str(response.text))['versions']['values']
+ except TypeError:
+ versions = self.deserialize(
+ headers, str(response.text))['versions']
LOG.info('Received enabled API versions for {} '
'user are:{}'.format(user, versions))
- for item in versions:
- enabled_ids = [
- item['id'] for key in item.keys() if key == 'id']
+
+ enabled_ids = {item['id'] for item in versions}
LOG.info('Enabled versions IDs are:{}'.format(enabled_ids))
- possible_options = [['v1'], ['v2'], ['v1', 'v2']]
- self.assertIn(
- enabled_ids, possible_options,
- 'Failed, received version: {} is not in possible options'
- ' list:{}'.format(enabled_ids, possible_options))
+
+ # Expect at least one of these to be present in the API
+ base_versions = {'v1', 'v2', 'v2.0'}
+ self.assertFalse(
+ enabled_ids.isdisjoint(base_versions),
+ 'Failed, at least one base API version: {} was not found in '
+ 'the API version list: {}'.format(base_versions, enabled_ids))
diff --git a/designate_tempest_plugin/tests/api/v2/test_pool.py b/designate_tempest_plugin/tests/api/v2/test_pool.py
index 60af204..dd7d107 100644
--- a/designate_tempest_plugin/tests/api/v2/test_pool.py
+++ b/designate_tempest_plugin/tests/api/v2/test_pool.py
@@ -36,7 +36,8 @@
class PoolAdminTest(BasePoolTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "primary", "system_admin", "system_reader",
+ "project_member", "project_reader", "alt"]
@classmethod
def setup_credentials(cls):
@@ -72,6 +73,16 @@
self.assertEqual(pool_data["name"], pool['name'])
self.assertExpected(pool_data, pool, self.excluded_keys)
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'PoolClient', 'create_pool', expected_allowed, False,
+ pool_name=pool_data["name"], ns_records=pool_data["ns_records"],
+ project_id=pool_data["project_id"])
+
@decorators.idempotent_id('e80eb70a-8ee5-40eb-b06e-599597a8ab7e')
def test_show_pool(self):
LOG.info('Create a pool')
@@ -88,6 +99,20 @@
self._assertExpectedNSRecords(pool["ns_records"], body["ns_records"],
expected_key="priority")
+ # TODO(johnsom) Test reader roles once this bug is fixed.
+ # https://bugs.launchpad.net/tempest/+bug/1964509
+ # Test RBAC
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ # TODO(johnsom) The pools API seems inconsistent with the requirement
+ # of the all-projects header.
+ self.check_list_show_RBAC_enforcement(
+ 'PoolClient', 'show_pool', expected_allowed, True, pool['id'],
+ headers=self.all_projects_header)
+
@decorators.idempotent_id('d8c4c377-5d88-452d-a4d2-c004d72e1abe')
def test_delete_pool(self):
LOG.info('Create a pool')
@@ -104,6 +129,14 @@
lambda: self.admin_client.show_pool(
pool['id'], headers=self.all_projects_header))
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'PoolClient', 'delete_pool', expected_allowed, False, pool['id'])
+
@decorators.idempotent_id('77c85b40-83b2-4c17-9fbf-e6d516cfce90')
def test_list_pools(self):
LOG.info('Create a pool')
@@ -117,6 +150,18 @@
self.assertGreater(len(body['pools']), 0)
+ # TODO(johnsom) Test reader roles once this bug is fixed.
+ # https://bugs.launchpad.net/tempest/+bug/1964509
+ # Test RBAC
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'PoolClient', 'list_pools', expected_allowed, [pool['id']],
+ headers=self.all_projects_header)
+
@decorators.idempotent_id('fdcc84ce-af65-4af6-a5fc-6c50acbea0f0')
def test_update_pool(self):
LOG.info('Create a pool')
@@ -131,6 +176,15 @@
self.assertEqual("foo", patch_pool["name"])
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'PoolClient', 'update_pool', expected_allowed, True,
+ pool['id'], pool_name="test-name")
+
@decorators.idempotent_id('41ad6a84-00ce-4a04-9fd5-b7c15c31e2db')
def test_list_pools_dot_json_fails(self):
uri = self.admin_client.get_uri('pools.json')
@@ -141,7 +195,7 @@
class TestPoolNotFoundAdmin(BasePoolTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
@@ -187,7 +241,7 @@
class TestPoolInvalidIdAdmin(BasePoolTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
@@ -234,7 +288,7 @@
class TestPoolAdminNegative(BasePoolTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
@@ -255,7 +309,7 @@
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)
+ pool_name=data_utils.rand_name(name="Huge_size_name") * 100)
@decorators.idempotent_id('9a787d0e-ac04-11eb-ae06-74e5f9e2a801')
def test_create_pool_invalid_hostname_in_ns_records(self):
@@ -283,7 +337,7 @@
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,
+ pool_name=data_utils.rand_name(name="Huge_size_name") * 100,
headers=self.all_projects_header, extra_headers=True)
@decorators.idempotent_id('2e496596-ac07-11eb-ae06-74e5f9e2a801')
diff --git a/designate_tempest_plugin/tests/api/v2/test_ptrs.py b/designate_tempest_plugin/tests/api/v2/test_ptrs.py
index b094a5d..dcb31a9 100644
--- a/designate_tempest_plugin/tests/api/v2/test_ptrs.py
+++ b/designate_tempest_plugin/tests/api/v2/test_ptrs.py
@@ -13,27 +13,72 @@
# under the License.
from oslo_log import log as logging
from tempest import config
+from tempest.lib.common.utils import test_utils
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
+import testtools
from designate_tempest_plugin.tests import base
from designate_tempest_plugin.common import constants as const
from designate_tempest_plugin.common import waiters
from designate_tempest_plugin import data_utils as dns_data_utils
-import tempest.test
+import tempest.api.network.base
CONF = config.CONF
LOG = logging.getLogger(__name__)
-TLD = dns_data_utils.rand_string(3)
-
class BasePtrTest(base.BaseDnsV2Test):
excluded_keys = ['created_at', 'updated_at', 'version', 'links',
'status', 'action']
+ @classmethod
+ def setup_clients(cls):
+ super(BasePtrTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.admin_network_client = cls.os_admin.networks_client
+ cls.admin_subnet_client = cls.os_admin.subnets_client
+
+ @classmethod
+ def resource_setup(cls):
+ super(BasePtrTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name='BasePtrTest')
+ cls.tld_name = tld_name[:-1]
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ # Create dedicated External network for PTRs tests
+ network_name = data_utils.rand_name('designate-tempest-fips-external')
+ cls.external_network = cls.admin_network_client.create_network(
+ name=network_name, **{'router:external': True})['network']
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.admin_network_client.delete_network,
+ cls.external_network['id'])
+
+ # Create subnet for External network
+ cls.external_subnet = cls.admin_subnet_client.create_subnet(
+ network_id=cls.external_network['id'],
+ allocation_pools=[
+ {'start': '198.51.100.10', 'end': '198.51.100.200'}],
+ cidr='198.51.100.0/24', ip_version=4, enable_dhcp=False)
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.admin_subnet_client.delete_subnet,
+ cls.external_subnet['subnet']['id'])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BasePtrTest, cls).resource_cleanup()
+
class DesignatePtrRecord(BasePtrTest, tempest.test.BaseTestCase):
@@ -55,11 +100,27 @@
cls.primary_ptr_client = cls.os_primary.dns_v2.PtrClient()
cls.primary_floating_ip_client = cls.os_primary.floating_ips_client
+ @classmethod
+ def resource_setup(cls):
+ super(DesignatePtrRecord, cls).resource_setup()
+
+ # The 'arpa' TLD is a special case as the negative test class also
+ # needs to use this space. To stop test class concurrency conflicts,
+ # let each class manage different TLDs for the reverse namespace.
+ cls.arpa_tld = cls.admin_tld_client.create_tld(tld_name='arpa')
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.arpa_tld[1]['id'])
+ super(DesignatePtrRecord, cls).resource_cleanup()
+
def _set_ptr(self, ptr_name=None, ttl=None, description=None,
- headers=None, tld=TLD, fip_id=None):
+ headers=None, tld=None, fip_id=None):
+ if not tld:
+ tld = self.tld_name
if not fip_id:
fip = self.primary_floating_ip_client.create_floatingip(
- floating_network_id=CONF.network.public_network_id)[
+ floating_network_id=self.external_network['id'])[
'floatingip']
fip_id = fip['id']
self.addCleanup(
@@ -67,7 +128,8 @@
ptr = self.primary_ptr_client.set_ptr_record(
fip_id, ptr_name=ptr_name, ttl=ttl, description=description,
headers=headers, tld=tld)
- self.addCleanup(self.primary_ptr_client.unset_ptr_record, fip_id)
+ self.addCleanup(self.unset_ptr, self.primary_ptr_client, fip_id)
+
self.assertEqual('CREATE', ptr['action'])
self.assertEqual('PENDING', ptr['status'])
waiters.wait_for_ptr_status(
@@ -77,7 +139,7 @@
def _unset_ptr(self, fip_id):
self.primary_ptr_client.unset_ptr_record(fip_id)
waiters.wait_for_ptr_status(
- self.primary_ptr_client, fip_id=fip_id, status=const.DELETED)
+ self.primary_ptr_client, fip_id=fip_id, status=const.INACTIVE)
@decorators.idempotent_id('2fb9d6ea-871d-11eb-9f9a-74e5f9e2a801')
def test_set_floatingip_ptr(self):
@@ -137,7 +199,8 @@
'Failed, expected ID was not found in "received_ptr_ids" list.')
@decorators.idempotent_id('499b5a7e-87e1-11eb-b412-74e5f9e2a801')
- @decorators.skip_because(bug="1932026")
+ @testtools.skipUnless(config.CONF.dns_feature_enabled.bug_1932026_fixed,
+ 'Skip unless bug 1932026 has been fixed.')
def test_unset_floatingip_ptr(self):
fip_id, ptr = self._set_ptr()
self._unset_ptr(fip_id)
@@ -145,7 +208,7 @@
class DesignatePtrRecordNegative(BasePtrTest, tempest.test.BaseTestCase):
- credentials = ['primary', 'admin']
+ credentials = ['primary', 'admin', 'system_admin']
@classmethod
def setup_credentials(cls):
@@ -160,11 +223,28 @@
cls.primary_floating_ip_client = cls.os_primary.floating_ips_client
cls.admin_ptr_client = cls.os_admin.dns_v2.PtrClient()
+ @classmethod
+ def resource_setup(cls):
+ super(DesignatePtrRecordNegative, cls).resource_setup()
+
+ # The 'arpa' TLD is a special case as the positive test class also
+ # needs to use this space. To stop test class concurrency conflicts,
+ # let each class manage different TLDs for the reverse namespace.
+ cls.in_addr_arpa_tld = cls.admin_tld_client.create_tld(
+ tld_name='in-addr.arpa')
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.in_addr_arpa_tld[1]['id'])
+ super(DesignatePtrRecordNegative, cls).resource_cleanup()
+
def _set_ptr(self, ptr_name=None, ttl=None, description=None,
- headers=None, tld=TLD, fip_id=None):
+ headers=None, tld=None, fip_id=None):
+ if not tld:
+ tld = self.tld_name
if not fip_id:
fip = self.primary_floating_ip_client.create_floatingip(
- floating_network_id=CONF.network.public_network_id)[
+ floating_network_id=self.external_network['id'])[
'floatingip']
fip_id = fip['id']
self.addCleanup(
@@ -172,7 +252,7 @@
ptr = self.primary_ptr_client.set_ptr_record(
fip_id, ptr_name=ptr_name, ttl=ttl, description=description,
headers=headers, tld=tld)
- self.addCleanup(self.primary_ptr_client.unset_ptr_record, fip_id)
+ self.addCleanup(self.unset_ptr, self.primary_ptr_client, fip_id)
self.assertEqual('CREATE', ptr['action'])
self.assertEqual('PENDING', ptr['status'])
waiters.wait_for_ptr_status(
diff --git a/designate_tempest_plugin/tests/api/v2/test_quotas.py b/designate_tempest_plugin/tests/api/v2/test_quotas.py
index db870af..37e07e3 100644
--- a/designate_tempest_plugin/tests/api/v2/test_quotas.py
+++ b/designate_tempest_plugin/tests/api/v2/test_quotas.py
@@ -15,7 +15,7 @@
from tempest import config
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
-from tempest.lib.common.utils import data_utils as tempest_data_utils
+from tempest.lib.common.utils import data_utils
from designate_tempest_plugin.tests import base
from designate_tempest_plugin import data_utils as dns_data_utils
@@ -23,10 +23,14 @@
CONF = config.CONF
LOG = logging.getLogger(__name__)
+quotas_types = ["api_export_size", "recordset_records",
+ "zone_records", "zone_recordsets", "zones"]
+
class QuotasV2Test(base.BaseDnsV2Test):
- credentials = ["primary", "admin", "system_admin", "alt"]
+ credentials = ["primary", "admin", "system_admin", "system_reader", "alt",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -53,7 +57,6 @@
cls.admin_client = cls.os_admin.dns_v2.QuotasClient()
cls.quotas_client = cls.os_primary.dns_v2.QuotasClient()
cls.alt_client = cls.os_alt.dns_v2.QuotasClient()
- cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
def _store_quotas(self, project_id, cleanup=True):
"""Remember current quotas and reset them after the test"""
@@ -68,45 +71,72 @@
@decorators.idempotent_id('1dac991a-9e2e-452c-a47a-26ac37381ec5')
def test_show_quotas(self):
- self._store_quotas(project_id=self.quotas_client.project_id)
- LOG.info("Updating quotas")
- quotas = dns_data_utils.rand_quotas()
- _, body = self.admin_client.update_quotas(
- project_id=self.quotas_client.project_id,
- headers=self.all_projects_header,
- **quotas)
+ LOG.info("Show default quotas, validate all quota types exists and "
+ "their values are integers.")
+ for user in ['primary', 'admin']:
+ if user == 'primary':
+ body = self.quotas_client.show_quotas()[1]
+ if user == 'admin':
+ body = self.admin_client.show_quotas(
+ project_id=self.quotas_client.project_id,
+ headers=self.all_projects_header)[1]
+ for quota_type in quotas_types:
+ self.assertIn(
+ quota_type, body.keys(),
+ 'Failed, expected quota type:{} was not found '
+ 'in received quota body'.format(quota_type))
+ for quota_type, quota_value in body.items():
+ self.assertTrue(
+ isinstance(quota_value, int),
+ 'Failed, the value of:{} is:{}, expected integer'.format(
+ quota_type, quota_value))
- LOG.info("Fetching quotas")
- _, body = self.admin_client.show_quotas(
- project_id=self.quotas_client.project_id,
- headers=self.all_projects_header)
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_system_reader',
+ 'os_project_member', 'os_project_reader'])
- LOG.info("Ensuring the response has all quota types")
- self.assertExpected(quotas, body, [])
+ self.check_list_show_with_ID_RBAC_enforcement(
+ 'QuotasClient', 'show_quotas', expected_allowed, False)
@decorators.idempotent_id('0448b089-5803-4ce3-8a6c-5c15ff75a2cc')
- def test_delete_quotas(self):
+ def test_reset_quotas(self):
self._store_quotas(project_id=self.quotas_client.project_id)
- LOG.info("Deleting quotas")
- _, body = self.admin_client.delete_quotas(
+
+ LOG.info("Deleting (reset) quotas")
+
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'QuotasClient', 'delete_quotas', expected_allowed, False,
+ project_id=self.quotas_client.project_id)
+
+ body = self.admin_client.delete_quotas(
project_id=self.quotas_client.project_id,
- headers=self.all_projects_header)
+ headers=self.all_projects_header)[1]
LOG.info("Ensuring an empty response body")
self.assertEqual(body.strip(), b"")
@decorators.idempotent_id('76d24c87-1b39-4e19-947c-c08e1380dc61')
def test_update_quotas(self):
- if CONF.enforce_scope.designate:
- raise self.skipException(
- "System scoped tokens do not have a project_id.")
-
- self._store_quotas(project_id=self.admin_client.project_id)
+ self._store_quotas(project_id=self.quotas_client.project_id)
LOG.info("Updating quotas")
quotas = dns_data_utils.rand_quotas()
- _, body = self.admin_client.update_quotas(
- project_id=self.admin_client.project_id,
- **quotas)
+ body = self.admin_client.update_quotas(
+ project_id=self.quotas_client.project_id,
+ **quotas, headers=self.all_projects_header)[1]
+
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'QuotasClient', 'update_quotas', expected_allowed, False,
+ project_id=self.quotas_client.project_id,
+ **quotas, headers=self.all_projects_header)
LOG.info("Ensuring the response has all quota types")
self.assertExpected(quotas, body, [])
@@ -121,15 +151,15 @@
quotas = dns_data_utils.rand_quotas()
request = quotas.copy()
- _, body = self.admin_client.update_quotas(
+ body = self.admin_client.update_quotas(
project_id=project_id,
headers=self.all_projects_header,
- **request)
+ **request)[1]
LOG.info("Ensuring the response has all quota types")
self.assertExpected(quotas, body, [])
- _, client_body = self.quotas_client.show_quotas(project_id=project_id)
+ client_body = self.quotas_client.show_quotas(project_id=project_id)[1]
self.assertExpected(quotas, client_body, [])
@@ -145,15 +175,15 @@
project_id=project_id,
headers=self.all_projects_header)
- _, default_quotas = self.admin_client.show_quotas(
+ default_quotas = self.admin_client.show_quotas(
project_id=project_id,
- headers=self.all_projects_header)
+ headers=self.all_projects_header)[1]
LOG.info("Updating quotas for %s ", project_id)
quotas = dns_data_utils.rand_quotas()
request = quotas.copy()
- _, body = self.admin_client.update_quotas(
+ self.admin_client.update_quotas(
project_id=project_id,
headers=self.all_projects_header,
**request)
@@ -162,9 +192,9 @@
project_id=project_id,
headers=self.all_projects_header)
- _, final_quotas = self.admin_client.show_quotas(
+ final_quotas = self.admin_client.show_quotas(
project_id=project_id,
- headers=self.all_projects_header)
+ headers=self.all_projects_header)[1]
self.assertExpected(default_quotas, final_quotas, [])
@@ -188,16 +218,45 @@
**request)
LOG.info("Make sure that the quotas weren't changed")
- _, client_body = self.quotas_client.show_quotas(
- project_id=self.quotas_client.project_id)
+ client_body = self.quotas_client.show_quotas(
+ project_id=self.quotas_client.project_id)[1]
self.assertExpected(original_quotas, client_body, [])
+
+class QuotasV2TestNegative(base.BaseDnsV2Test):
+
+ credentials = ["primary", "admin", "system_admin"]
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(QuotasV2TestNegative, cls).setup_credentials()
+
+ @classmethod
+ def skip_checks(cls):
+ super(QuotasV2TestNegative, cls).skip_checks()
+
+ if not CONF.dns_feature_enabled.api_v2_quotas:
+ skip_msg = ("%s skipped as designate V2 Quotas API is not "
+ "available" % cls.__name__)
+ raise cls.skipException(skip_msg)
+
+ @classmethod
+ def setup_clients(cls):
+ super(QuotasV2TestNegative, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_client = cls.os_system_admin.dns_v2.QuotasClient()
+ else:
+ cls.admin_client = cls.os_admin.dns_v2.QuotasClient()
+ cls.quotas_client = cls.os_primary.dns_v2.QuotasClient()
+
@decorators.idempotent_id('ae82a0ba-da60-11eb-bf12-74e5f9e2a801')
def test_admin_sets_quota_for_a_project(self):
primary_project_id = self.quotas_client.project_id
- http_headers_to_use = [
- {'X-Auth-All-Projects': True},
+ http_headers_to_use = [self.all_projects_header,
{'x-auth-sudo-project-id': primary_project_id}]
for http_header in http_headers_to_use:
@@ -240,21 +299,32 @@
lib_exc.Forbidden, self.quotas_client.set_quotas,
project_id=self.quotas_client.project_id,
quotas=dns_data_utils.rand_quotas(),
- headers={'x-auth-all-projects': True})
+ headers=self.all_projects_header)
@decorators.idempotent_id('a6ce5b46-dcce-11eb-903e-74e5f9e2a801')
- @decorators.skip_because(bug="1934596")
def test_admin_sets_invalid_quota_values(self):
primary_project_id = self.quotas_client.project_id
- http_header = {'X-Auth-All-Projects': True}
- for item in ['zones', 'zone_records',
- 'zone_recordsets', 'recordset_records']:
+ for item in quotas_types:
quota = dns_data_utils.rand_quotas()
- quota[item] = tempest_data_utils.rand_name()
+ quota[item] = data_utils.rand_name()
self.assertRaises(
lib_exc.BadRequest, self.admin_client.set_quotas,
project_id=primary_project_id,
quotas=quota,
- headers=http_header)
+ headers=self.all_projects_header)
+
+ @decorators.idempotent_id('ac212fd8-c602-11ec-b042-201e8823901f')
+ def test_admin_sets_not_existing_quota_type(self):
+
+ LOG.info('Try to set quota using not existing quota type in its body')
+ primary_project_id = self.quotas_client.project_id
+ quota = dns_data_utils.rand_quotas()
+ quota[data_utils.rand_name()] = 777
+
+ with self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_object', 400):
+ self.admin_client.set_quotas(
+ project_id=primary_project_id,
+ quotas=quota, headers=self.all_projects_header)
diff --git a/designate_tempest_plugin/tests/api/v2/test_recordset.py b/designate_tempest_plugin/tests/api/v2/test_recordset.py
index 38a2dfd..a7cb4bc 100644
--- a/designate_tempest_plugin/tests/api/v2/test_recordset.py
+++ b/designate_tempest_plugin/tests/api/v2/test_recordset.py
@@ -15,14 +15,13 @@
from tempest import config
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
-from tempest.lib.common.utils import data_utils as lib_data_utils
-import ddt
+from tempest.lib.common.utils import data_utils
from designate_tempest_plugin.tests import base
from designate_tempest_plugin.common import constants as const
from designate_tempest_plugin.common import waiters
-from designate_tempest_plugin import data_utils
+from designate_tempest_plugin import data_utils as dns_data_utils
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -33,25 +32,40 @@
'type']
@classmethod
+ def setup_clients(cls):
+ super(BaseRecordsetsTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
def resource_setup(cls):
super(BaseRecordsetsTest, cls).resource_setup()
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseRecordsetsTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
# All the recordset tests need a zone, create one to share
- LOG.info('Create a zone')
- _, cls.zone = cls.zone_client.create_zone()
+ zone_name = dns_data_utils.rand_zone_name(name="TestZone",
+ suffix=cls.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ cls.zone = cls.zones_client.create_zone(name=zone_name)[1]
@classmethod
def resource_cleanup(cls):
- cls.zone_client.delete_zone(
+ cls.zones_client.delete_zone(
cls.zone['id'], ignore_errors=lib_exc.NotFound)
-
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
super(BaseRecordsetsTest, cls).resource_cleanup()
-@ddt.ddt
class RecordsetsTest(BaseRecordsetsTest):
- credentials = ["admin", "system_admin", "primary", "alt"]
+ credentials = ["admin", "system_admin", "system_reader", "primary", "alt",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -70,15 +84,24 @@
cls.admin_zone_client = cls.os_admin.dns_v2.ZonesClient()
cls.client = cls.os_primary.dns_v2.RecordsetClient()
cls.alt_client = cls.os_alt.dns_v2.RecordsetClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
@decorators.attr(type='smoke')
@decorators.idempotent_id('631d74fd-6909-4684-a61b-5c4d2f92c3e7')
def test_create_recordset(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+
+ self.check_CUD_RBAC_enforcement(
+ 'RecordsetClient', 'create_recordset', expected_allowed, True,
+ self.zone['id'], recordset_data)
+
LOG.info('Create a Recordset')
resp, body = self.client.create_recordset(
self.zone['id'], recordset_data)
@@ -127,16 +150,18 @@
@decorators.idempotent_id('6c22a3f9-3f4d-4b32-bdf2-5237851ed25e')
def test_create_recordset_type_SRV_TCP(self):
self._test_create_recordset_type(
- "_sip._tcp", "SRV", ["10 60 5060 server1.example.com.",
- "20 60 5060 server2.example.com.",
- "20 30 5060 server3.example.com."])
+ "_sip._tcp", "SRV", [
+ "10 60 5060 server1.example{}".format(self.tld_name),
+ "20 60 5060 server2.example{}".format(self.tld_name),
+ "20 30 5060 server3.example{}".format(self.tld_name)])
@decorators.idempotent_id('59c1aa42-278e-4f7b-a6a1-4320d5daf1fd')
def test_create_recordset_type_SRV_UDP(self):
self._test_create_recordset_type(
- "_sip._udp", "SRV", ["10 60 5060 server1.example.com.",
- "10 60 5060 server2.example.com.",
- "20 30 5060 server3.example.com."])
+ "_sip._udp", "SRV", [
+ "10 60 5060 server1.example{}".format(self.tld_name),
+ "10 60 5060 server2.example{}".format(self.tld_name),
+ "20 30 5060 server3.example{}".format(self.tld_name)])
@decorators.idempotent_id('1ac46f94-f03a-4f85-b84f-826a2660b927')
def test_create_recordset_type_CNAME(self):
@@ -163,9 +188,7 @@
self._test_create_recordset_type(
"www", "TXT", ["\"Any Old Text Goes Here\""])
- @decorators.idempotent_id('69f002e5-6511-43d3-abae-7abdd45ae03e')
- @ddt.file_data("recordset_wildcard_data.json")
- def test_create_wildcard_recordset(self, name, type, records):
+ def _test_create_wildcard_recordset(self, name, type, records):
if name is not None:
recordset_name = name + "." + self.zone['name']
@@ -188,101 +211,259 @@
LOG.info('Ensure we respond with PENDING')
self.assertEqual(const.PENDING, body['status'])
+ @decorators.idempotent_id('69f002e5-6511-43d3-abae-7abdd45ae03e')
+ def test_create_wildcard_recordset_A_at_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*", "A", ["192.0.2.1", "192.0.2.2", "192.0.2.3"])
+
+ @decorators.idempotent_id('d97ee452-0dc3-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_A_under_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*.sub", "A", ["192.0.2.1", "192.0.2.2", "192.0.2.3"])
+
+ @decorators.idempotent_id('1b3c1cc0-0dc4-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_AAAA_at_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*", "AAAA", ["2001:db8::1", "2001:db8::1", "2001:db8::"])
+
+ @decorators.idempotent_id('928e735e-0dc4-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_AAAA_under_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*.sub", "AAAA", ["2001:db8::1", "2001:db8::1", "2001:db8::"])
+
+ @decorators.idempotent_id('d96138f2-0dc4-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_MX_at_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*", "MX", ["10 mail1.example.org.", "20 mail2.example.org."])
+
+ @decorators.idempotent_id('ff273c94-0dc4-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_MX_under_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*.sub", "MX", ["10 mail.example.org."])
+
+ @decorators.idempotent_id('3097f16a-0dc5-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_SPF_at_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*", "SPF", ["\"v=spf1; a -all\""])
+
+ @decorators.idempotent_id('50b3f390-0dc5-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_SPF_under_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*.sub", "SPF", ["\"v=spf1; a -all\""])
+
+ @decorators.idempotent_id('5b73981c-0dc5-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_TXT_at_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*", "TXT", ["\"Can you read me?\""])
+
+ @decorators.idempotent_id('5b73981c-0dc5-11ee-8b75-201e8823901f')
+ def test_create_wildcard_recordset_TXT_under_APEX(self):
+ self._test_create_wildcard_recordset(
+ "*.sub", "TXT", ["\"Can you read me?\""])
+
@decorators.idempotent_id('5964f730-5546-46e6-9105-5030e9c492b2')
def test_list_recordsets(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
LOG.info('Create a Recordset')
resp, body = self.client.create_recordset(
self.zone['id'], recordset_data)
+ recordset_id = body['id']
self.addCleanup(
self.wait_recordset_delete, self.client,
- self.zone['id'], body['id'])
+ self.zone['id'], recordset_id)
LOG.info('List zone recordsets')
- _, body = self.client.list_recordset(self.zone['id'])
+ body = self.client.list_recordset(self.zone['id'])[1]
self.assertGreater(len(body), 0)
+ # Test RBAC
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_reader',
+ 'os_project_member'])
+
+ self.check_list_show_RBAC_enforcement(
+ 'RecordsetClient', 'list_recordset', expected_allowed, True,
+ self.zone['id'])
+
+ # Test that users who should see the zone, can see it.
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_reader',
+ 'os_project_member'])
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'RecordsetClient', 'list_recordset',
+ expected_allowed, [recordset_id], self.zone['id'])
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'RecordsetClient', 'list_recordset', expected_allowed,
+ [recordset_id], self.zone['id'], headers=self.all_projects_header)
+ self.check_list_IDs_RBAC_enforcement(
+ 'RecordsetClient', 'list_recordset',
+ expected_allowed, [recordset_id], self.zone['id'],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('84c13cb2-9020-4c1e-aeb0-c348d9a70caa')
def test_show_recordsets(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
LOG.info('Create a Recordset')
resp, body = self.client.create_recordset(
self.zone['id'], recordset_data)
+ recordset_id = body['id']
self.addCleanup(
self.wait_recordset_delete, self.client,
- self.zone['id'], body['id'])
+ self.zone['id'], recordset_id)
LOG.info('Re-Fetch the Recordset')
- _, record = self.client.show_recordset(self.zone['id'], body['id'])
+ record = self.client.show_recordset(self.zone['id'], recordset_id)[1]
LOG.info('Ensure the fetched response matches the expected one')
self.assertExpected(body, record, self.excluded_keys)
+ # Test RBAC
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_member',
+ 'os_project_reader'])
+
+ self.check_list_show_RBAC_enforcement(
+ 'RecordsetClient', 'show_recordset', expected_allowed, True,
+ self.zone['id'], recordset_id)
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'RecordsetClient', 'show_recordset', expected_allowed, True,
+ self.zone['id'], recordset_id, headers=self.all_projects_header)
+ self.check_list_show_RBAC_enforcement(
+ 'RecordsetClient', 'show_recordset', expected_allowed, True,
+ self.zone['id'], recordset_id,
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('855399c1-8806-4ae5-aa31-cb8a6f35e218')
def test_delete_recordset(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
LOG.info('Create a Recordset')
- _, record = self.client.create_recordset(
- self.zone['id'], recordset_data)
+ record = self.client.create_recordset(
+ self.zone['id'], recordset_data)[1]
+ recordset_id = record['id']
self.addCleanup(
self.wait_recordset_delete, self.client,
- self.zone['id'], record['id'])
+ self.zone['id'], recordset_id)
+
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'RecordsetClient', 'delete_recordset', expected_allowed, True,
+ self.zone['id'], recordset_id)
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'RecordsetClient', 'delete_recordset', expected_allowed, False,
+ self.zone['id'], recordset_id, headers=self.all_projects_header)
+
+ self.check_CUD_RBAC_enforcement(
+ 'RecordsetClient', 'delete_recordset', expected_allowed, False,
+ self.zone['id'], recordset_id,
+ headers={'x-auth-sudo-project-id': self.client.project_id})
LOG.info('Delete a Recordset')
- _, body = self.client.delete_recordset(self.zone['id'], record['id'])
+ self.client.delete_recordset(self.zone['id'], recordset_id)
LOG.info('Ensure successful deletion of Recordset')
self.assertRaises(lib_exc.NotFound,
- lambda: self.client.show_recordset(self.zone['id'], record['id']))
+ lambda: self.client.show_recordset(self.zone['id'], recordset_id))
@decorators.idempotent_id('8d41c85f-09f9-48be-a202-92d1bdf5c796')
def test_update_recordset(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
LOG.info('Create a recordset')
- _, record = self.client.create_recordset(
- self.zone['id'], recordset_data)
+ record = self.client.create_recordset(
+ self.zone['id'], recordset_data)[1]
+ recordset_id = record['id']
self.addCleanup(
self.wait_recordset_delete, self.client,
- self.zone['id'], record['id'])
+ self.zone['id'], recordset_id)
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'], name=record['name'])
LOG.info('Update the recordset')
- _, update = self.client.update_recordset(self.zone['id'],
- record['id'], recordset_data)
+ update = self.client.update_recordset(self.zone['id'],
+ recordset_id, recordset_data)[1]
self.assertEqual(record['name'], update['name'])
self.assertNotEqual(record['records'], update['records'])
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'RecordsetClient', 'update_recordset', expected_allowed, True,
+ self.zone['id'], recordset_id, recordset_data)
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'RecordsetClient', 'update_recordset', expected_allowed, False,
+ self.zone['id'], recordset_id, recordset_data,
+ headers=self.all_projects_header)
+ self.check_CUD_RBAC_enforcement(
+ 'RecordsetClient', 'update_recordset', expected_allowed, False,
+ self.zone['id'], recordset_id, recordset_data,
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('60904cc5-148b-4e3b-a0c6-35656dc8d44c')
def test_update_recordset_one_field(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
LOG.info('Create a recordset')
- _, record = self.client.create_recordset(
- self.zone['id'], recordset_data)
+ record = self.client.create_recordset(
+ self.zone['id'], recordset_data)[1]
self.addCleanup(
self.wait_recordset_delete, self.client,
self.zone['id'], record['id'])
recordset_data = {
- 'ttl': data_utils.rand_ttl(start=record['ttl'] + 1)
+ 'ttl': dns_data_utils.rand_ttl(start=record['ttl'] + 1)
}
LOG.info('Update the recordset')
- _, update = self.client.update_recordset(self.zone['id'],
- record['id'], recordset_data)
+ update = self.client.update_recordset(self.zone['id'],
+ record['id'], recordset_data)[1]
self.assertEqual(record['name'], update['name'])
self.assertEqual(record['records'], update['records'])
@@ -293,7 +474,7 @@
def test_show_recordsets_impersonate_another_project(self):
LOG.info('Create a Recordset')
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
resp, body = self.client.create_recordset(
self.zone['id'], recordset_data)
@@ -333,7 +514,7 @@
def test_admin_list_all_recordsets_for_a_project(self):
LOG.info('Create a Recordset as Primary tenant')
- recordset_data_primary_1 = data_utils.rand_recordset_data(
+ recordset_data_primary_1 = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
body_pr_1 = self.client.create_recordset(
self.zone['id'], recordset_data_primary_1)[1]
@@ -342,7 +523,7 @@
self.zone['id'], body_pr_1['id'])
self.assertEqual(const.PENDING, body_pr_1['status'],
'Failed, expected status is PENDING')
- recordset_data_primary_2 = data_utils.rand_recordset_data(
+ recordset_data_primary_2 = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
body_pr_2 = self.client.create_recordset(
self.zone['id'], recordset_data_primary_2)[1]
@@ -385,9 +566,9 @@
@decorators.idempotent_id('48013b7c-f526-11eb-b04f-74e5f9e2a801')
def test_create_A_recordset_multiply_ips(self):
LOG.info('Create A type Recordset using a list of random IPs')
- recordset_data = data_utils.rand_a_recordset(
+ recordset_data = dns_data_utils.rand_a_recordset(
zone_name=self.zone['name'],
- ips=[data_utils.rand_ip() for _ in range(10)])
+ ips=[dns_data_utils.rand_ip() for _ in range(10)])
resp, body = self.client.create_recordset(
self.zone['id'], recordset_data)
self.addCleanup(
@@ -400,11 +581,39 @@
self.client, self.zone['id'],
body['id'], const.ACTIVE)
+ @decorators.idempotent_id('f15e583e-e479-11eb-8e5a-74e5f9e2a801')
+ def test_delete_zone_with_existing_recordset(self):
-@ddt.ddt
+ LOG.info('Create a Zone')
+ zone_name = dns_data_utils.rand_zone_name(name="TestZone",
+ suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ LOG.info('Create a Recordset')
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ record = self.client.create_recordset(
+ zone['id'], recordset_data, wait_until=const.ACTIVE)[1]
+
+ LOG.info("Delete a Zone and wait till it's done")
+ body = self.zones_client.delete_zone(zone['id'])[1]
+ LOG.info('Ensure we respond with DELETE+PENDING')
+ self.assertEqual(const.DELETE, body['action'])
+ self.assertEqual(const.PENDING, body['status'])
+
+ LOG.info('Ensure successful deletion of Zone')
+ waiters.wait_for_zone_404(self.zones_client, zone['id'])
+
+ LOG.info('Ensure successful deletion of Recordset')
+ self.assertRaises(lib_exc.NotFound,
+ lambda: self.client.show_recordset(zone['id'], record['id']))
+
+
class RecordsetsNegativeTest(BaseRecordsetsTest):
- credentials = ["primary", "alt"]
+ credentials = ["admin", "system_admin", "primary", "alt"]
@classmethod
def setup_credentials(cls):
@@ -417,11 +626,8 @@
super(RecordsetsNegativeTest, cls).setup_clients()
cls.client = cls.os_primary.dns_v2.RecordsetClient()
cls.alt_client = cls.os_alt.dns_v2.RecordsetClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
- @decorators.idempotent_id('98c94f8c-217a-4056-b996-b1f856d0753e')
- @ddt.file_data("recordset_data_invalid.json")
- def test_create_recordset_invalid(self, name, type, records):
+ def _test_create_recordset_invalid(self, name, type, records):
if name is not None:
recordset_name = name + "." + self.zone['name']
@@ -439,81 +645,70 @@
lambda: self.client.create_recordset(
self.zone['id'], recordset_data))
+ @decorators.idempotent_id('98c94f8c-217a-4056-b996-b1f856d0753e')
+ def test_create_recordset_invalid_CNAME_multiple(self):
+ self._test_create_recordset_invalid(
+ 'www', 'CNAME', ["target1.example.org.", "target2.example.org."])
+
+ @decorators.idempotent_id('43385ce8-0dd3-11ee-8b75-201e8823901f')
+ def test_create_recordset_invalid_CNAME_at_Apex(self):
+ self._test_create_recordset_invalid(
+ None, 'CNAME', ["target1.example.org."])
+
@decorators.idempotent_id('b6dad57e-5ce9-4fa5-8d66-aebbcd23b4ad')
def test_get_nonexistent_recordset(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
LOG.info('Attempt to get an invalid Recordset')
with self.assertRaisesDns(
lib_exc.NotFound, 'recordset_not_found', 404):
- self.client.show_recordset(zone['id'], lib_data_utils.rand_uuid())
+ self.client.show_recordset(self.zone['id'],
+ data_utils.rand_uuid())
@decorators.idempotent_id('93d744a8-0dfd-4650-bcef-1e6ad632ad72')
def test_get_nonexistent_recordset_invalid_id(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
LOG.info('Attempt to get an invalid Recordset')
with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_uuid', 400):
- self.client.show_recordset(zone['id'], 'invalid')
+ self.client.show_recordset(self.zone['id'], 'invalid')
@decorators.idempotent_id('da08f19a-7f10-47cc-8b41-994507190812')
def test_update_nonexistent_recordset(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
- recordset_data = data_utils.rand_recordset_data('A', zone['name'])
+ recordset_data = dns_data_utils.rand_recordset_data(
+ 'A', self.zone['name'])
LOG.info('Attempt to update an invalid Recordset')
with self.assertRaisesDns(
lib_exc.NotFound, 'recordset_not_found', 404):
self.client.update_recordset(
- zone['id'], lib_data_utils.rand_uuid(), recordset_data)
+ self.zone['id'], data_utils.rand_uuid(), recordset_data)
@decorators.idempotent_id('158340a1-3f69-4aaa-9968-956190563768')
def test_update_nonexistent_recordset_invalid_id(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
- recordset_data = data_utils.rand_recordset_data('A', zone['name'])
+ recordset_data = dns_data_utils.rand_recordset_data(
+ 'A', self.zone['name'])
LOG.info('Attempt to update an invalid Recordset')
with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_uuid', 400):
self.client.update_recordset(
- zone['id'], 'invalid', recordset_data)
+ self.zone['id'], 'invalid', recordset_data)
@decorators.idempotent_id('64bd94d4-54bd-4bee-b6fd-92ede063234e')
def test_delete_nonexistent_recordset(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
LOG.info('Attempt to delete an invalid Recordset')
with self.assertRaisesDns(
lib_exc.NotFound, 'recordset_not_found', 404):
self.client.delete_recordset(
- zone['id'], lib_data_utils.rand_uuid())
+ self.zone['id'], data_utils.rand_uuid())
@decorators.idempotent_id('5948b599-a332-4dcb-840b-afc825075ba3')
def test_delete_nonexistent_recordset_invalid_id(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
LOG.info('Attempt to get an invalid Recordset')
with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_uuid', 400):
- self.client.delete_recordset(zone['id'], 'invalid')
+ self.client.delete_recordset(self.zone['id'], 'invalid')
@decorators.idempotent_id('64e01dc4-a2a8-11eb-aad4-74e5f9e2a801')
def test_show_recordsets_invalid_ids(self):
LOG.info('Create a Recordset')
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
resp, body = self.client.create_recordset(
self.zone['id'], recordset_data)
@@ -532,18 +727,18 @@
self.assertRaises(
lib_exc.NotFound, lambda: self.client.show_recordset(
zone_uuid=self.zone['id'],
- recordset_uuid=lib_data_utils.rand_uuid()))
+ recordset_uuid=data_utils.rand_uuid()))
LOG.info('Ensure 404 NotFound status code is received if '
'zone ID is invalid.')
self.assertRaises(
lib_exc.NotFound, lambda: self.client.show_recordset(
- zone_uuid=lib_data_utils.rand_uuid(),
+ zone_uuid=data_utils.rand_uuid(),
recordset_uuid=body['id']))
@decorators.idempotent_id('c1d9f046-a2b1-11eb-aad4-74e5f9e2a801')
def test_create_recordset_for_other_tenant(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
LOG.info('Create a Recordset as Alt tenant for a zone created by '
@@ -554,6 +749,8 @@
class RootRecordsetsTests(BaseRecordsetsTest):
+ credentials = ["admin", "primary", "system_admin", "alt"]
+
@classmethod
def setup_credentials(cls):
# Do not create network resources for these test.
@@ -564,7 +761,6 @@
def setup_clients(cls):
super(RootRecordsetsTests, cls).setup_clients()
cls.client = cls.os_primary.dns_v2.RecordsetClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
@classmethod
def skip_checks(cls):
@@ -578,13 +774,13 @@
@decorators.idempotent_id('48a081b9-4474-4da0-9b1a-6359a80456ce')
def test_list_zones_recordsets(self):
LOG.info('List recordsets')
- _, body = self.client.list_zones_recordsets()
+ body = self.client.list_zones_recordsets()[1]
self.assertGreater(len(body['recordsets']), 0)
@decorators.idempotent_id('65ec0495-81d9-4cfb-8007-9d93b32ae883')
def test_get_single_zones_recordsets(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'], records=['10.1.0.2'])
LOG.info('Create a Recordset')
@@ -598,7 +794,7 @@
@decorators.idempotent_id('a8e41020-65be-453b-a8c1-2497d539c345')
def test_list_filter_zones_recordsets(self):
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'], records=['10.0.1.2'])
LOG.info('Create a Recordset')
@@ -609,11 +805,13 @@
self.zone['id'], zone_recordset['id'])
LOG.info('Create another zone')
- _, zone2 = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone2['id'])
+ zone_name = dns_data_utils.rand_zone_name(name="list-filter",
+ suffix=self.tld_name)
+ zone2 = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone2['id'])
LOG.info('Create another Recordset')
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=zone2['name'],
records=['10.0.1.3'])
resp, zone_recordset2 = self.client.create_recordset(
@@ -623,7 +821,7 @@
self.zone['id'], zone_recordset2['id'])
LOG.info('List recordsets')
- _, body = self.client.list_zones_recordsets(params={"data": "10.0.*"})
+ body = self.client.list_zones_recordsets(params={"data": "10.0.*"})[1]
recordsets = body['recordsets']
@@ -640,12 +838,16 @@
@decorators.idempotent_id('7f4970bf-9aeb-4a3c-9afd-02f5a7178d35')
def test_list_zones_recordsets_zone_names(self):
- LOG.info('Create another zone')
- _, zone2 = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone2['id'])
-
LOG.info('List recordsets')
- _, body = self.client.list_zones_recordsets()
+ zone_name = dns_data_utils.rand_zone_name(name="zone_names",
+ suffix=self.tld_name)
+ alt_zone = self.zones_client.create_zone(
+ name=zone_name, wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete,
+ self.zones_client,
+ alt_zone['id'])
+
+ body = self.client.list_zones_recordsets()[1]
recordsets = body['recordsets']
zone_names = set()
@@ -674,7 +876,6 @@
cls.admin_client = cls.os_admin.dns_v2.RecordsetClient()
cls.client = cls.os_primary.dns_v2.RecordsetClient()
cls.alt_client = cls.os_alt.dns_v2.RecordsetClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
def _create_client_recordset(self, clients_list):
@@ -686,23 +887,27 @@
for client in clients_list:
if client == 'primary':
# Create a zone and wait till it's ACTIVE
- zone = self.zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(name="primary",
+ suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(self.wait_zone_delete,
- self.zone_client,
+ self.zones_client,
zone['id'])
waiters.wait_for_zone_status(
- self.zone_client, zone['id'], const.ACTIVE)
+ self.zones_client, zone['id'], const.ACTIVE)
# Create a recordset and wait till it's ACTIVE
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=zone['name'])
resp, body = self.client.create_recordset(
zone['id'], recordset_data)
+
self.addCleanup(
self.wait_recordset_delete, self.client,
self.zone['id'], body['id'])
self.assertEqual(const.PENDING, body['status'],
'Failed, expected status is PENDING')
+
LOG.info('Wait until the recordset is active')
waiters.wait_for_recordset_status(
self.client, zone['id'],
@@ -714,7 +919,9 @@
if client == 'alt':
# Create a zone and wait till it's ACTIVE
- alt_zone = self.alt_zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(name="alt",
+ suffix=self.tld_name)
+ alt_zone = self.alt_zone_client.create_zone(name=zone_name)[1]
self.addCleanup(self.wait_zone_delete,
self.alt_zone_client,
alt_zone['id'])
@@ -722,15 +929,17 @@
self.alt_zone_client, alt_zone['id'], const.ACTIVE)
# Create a recordset and wait till it's ACTIVE
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=alt_zone['name'])
resp, body = self.alt_client.create_recordset(
alt_zone['id'], recordset_data)
+
self.addCleanup(
self.wait_recordset_delete, self.client,
self.zone['id'], body['id'])
self.assertEqual(const.PENDING, body['status'],
'Failed, expected status is PENDING')
+
LOG.info('Wait until the recordset is active')
waiters.wait_for_recordset_status(
self.alt_client, alt_zone['id'],
@@ -740,12 +949,13 @@
recordset_data['project_id'] = alt_zone['project_id']
recordsets_created['alt'] = recordset_data
+ LOG.info('Created resordsets are {}:'.format(recordsets_created))
return recordsets_created
@decorators.idempotent_id('9c0f58ad-1b31-4899-b184-5380720604e5')
def test_no_create_recordset_by_alt_tenant(self):
# try with name=A123456.zone.com.
- recordset_data = data_utils.rand_recordset_data(
+ recordset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=self.zone['name'])
resp, rrset = self.client.create_recordset(
self.zone['id'], recordset_data)
@@ -760,13 +970,13 @@
@decorators.idempotent_id('d4a9aad9-c778-429b-9a0c-4cd2b61a0a01')
def test_no_create_super_recordsets(self):
- zone_name = data_utils.rand_zone_name()
+ zone_name = dns_data_utils.rand_zone_name(suffix=self.tld_name)
LOG.info('Create a zone as a default user')
- _, zone = self.zone_client.create_zone(name='a.b.' + zone_name)
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone = self.zones_client.create_zone(name='a.b.' + zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
- rrset_data = data_utils.rand_recordset_data(
+ rrset_data = dns_data_utils.rand_recordset_data(
record_type='A', zone_name=zone_name)
LOG.info('Create a zone as an alt user with existing superdomain')
@@ -777,23 +987,21 @@
@decorators.idempotent_id('3dbe244d-fa85-4afc-869b-0306388d8746')
def test_no_create_recordset_via_alt_domain(self):
- _, zone = self.zone_client.create_zone()
- _, alt_zone = self.alt_zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete,
- self.zone_client,
- zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(name="alt-domain",
+ suffix=self.tld_name)
+ alt_zone = self.alt_zone_client.create_zone(name=zone_name)[1]
self.addCleanup(self.wait_zone_delete,
self.alt_zone_client,
alt_zone['id'])
# alt attempts to create record with name A12345.{zone}
- recordset_data = data_utils.rand_recordset_data(
- record_type='A', zone_name=zone['name'])
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=self.zone['name'])
self.assertRaises(
lib_exc.RestClientException,
lambda: self.alt_client.create_recordset(
- zone['id'],
+ self.zone['id'],
recordset_data
)
)
@@ -860,3 +1068,110 @@
{primary_project_id}, project_ids_api,
'Failed, unique project_ids {} are not as expected {}'.format(
project_ids_api, primary_project_id))
+
+
+class AdminManagedRecordsetTest(BaseRecordsetsTest):
+
+ credentials = ["primary", "admin", "system_admin"]
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(AdminManagedRecordsetTest, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(AdminManagedRecordsetTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_client = cls.os_system_admin.dns_v2.RecordsetClient()
+ else:
+ cls.admin_client = cls.os_admin.dns_v2.RecordsetClient()
+ cls.client = cls.os_primary.dns_v2.RecordsetClient()
+
+ @decorators.idempotent_id('84164ff4-8e68-11ec-983f-201e8823901f')
+ def test_admin_updates_soa_and_ns_recordsets(self):
+ # HTTP headers to be used in the test
+ sudo_header = {'X-Auth-All-Projects': True}
+ managed_records_header = {'X-Designate-Edit-Managed-Records': True}
+ sudo_managed_headers = sudo_header.copy()
+ sudo_managed_headers.update(managed_records_header)
+
+ LOG.info('Primary user creates a Zone')
+ zone_name = dns_data_utils.rand_zone_name(name="update_soa_ns",
+ suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name,
+ description='Zone for "managed recordsets update" test',
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ recordsets = self.admin_client.list_recordset(
+ zone['id'], headers=sudo_header)[1]['recordsets']
+
+ LOG.info('As Admin try to update SOA and NS recordsets,'
+ ' Expected not allowed')
+ for recordset in recordsets:
+ if recordset['type'] == 'NS':
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'bad_request', 400,
+ self.admin_client.update_recordset,
+ zone['id'], recordset['id'],
+ recordset_data=dns_data_utils.rand_ns_records(),
+ headers=sudo_managed_headers, extra_headers=True)
+
+ if recordset['type'] == 'SOA':
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'bad_request', 400,
+ self.admin_client.update_recordset,
+ zone['id'], recordset['id'],
+ recordset_data=dns_data_utils.rand_soa_recordset(
+ zone['name']),
+ headers=sudo_managed_headers, extra_headers=True)
+
+
+class RecordsetsManagedRecordsNegativeTest(BaseRecordsetsTest):
+
+ credentials = ["admin", "system_admin", "primary"]
+
+ @classmethod
+ def setup_clients(cls):
+ super(RecordsetsManagedRecordsNegativeTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_client = cls.os_system_admin.dns_v2.RecordsetClient()
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_client = cls.os_admin.dns_v2.RecordsetClient()
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
+
+ @decorators.idempotent_id('083fa738-bb1b-11ec-b581-201e8823901f')
+ def test_delete_ns_record_not_permitted(self):
+ LOG.info('Get NS type recordset ID')
+ recordsets = self.recordset_client.list_recordset(
+ self.zone['id'])[1]['recordsets']
+ for recordset in recordsets:
+ if recordset['type'] == 'NS':
+ ns_record_id = recordset['id']
+ break
+
+ LOG.info('Primary user tries to delete NS Recordset')
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.recordset_client.delete_recordset,
+ self.zone['id'], ns_record_id, headers=self.managed_records)
+
+ @decorators.idempotent_id('1e78a742-66ee-11ec-8dc3-201e8823901f')
+ def test_create_soa_record_not_permitted(self):
+ soa_record = ("s1.devstack.org. admin.example.net. 1510721487 3510"
+ " 600 86400 3600")
+ LOG.info('Primary tries to create a Recordset on '
+ 'the existing zone')
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.recordset_client.create_recordset,
+ self.zone['id'], soa_record)
+ LOG.info('Admin tries to create a Recordset on the existing zone')
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.admin_client.create_recordset,
+ self.zone['id'], soa_record)
diff --git a/designate_tempest_plugin/tests/api/v2/test_recordset_validation.py b/designate_tempest_plugin/tests/api/v2/test_recordset_validation.py
index a7efe31..e61e7fa 100644
--- a/designate_tempest_plugin/tests/api/v2/test_recordset_validation.py
+++ b/designate_tempest_plugin/tests/api/v2/test_recordset_validation.py
@@ -13,15 +13,19 @@
See the License for the specific language governing permissions and
limitations under the License.
"""
-import ddt
-from tempest.lib import exceptions
+from oslo_log import log as logging
+
+from tempest import config
from tempest.lib import decorators
+from tempest.lib import exceptions
from designate_tempest_plugin.tests import base
from designate_tempest_plugin.common import waiters
-from designate_tempest_plugin import data_utils
+from designate_tempest_plugin import data_utils as dns_data_utils
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
RECORDSETS_DATASET = [
'A',
'AAAA',
@@ -32,11 +36,29 @@
'SSHFP',
'TXT',
]
+INVALID_TXT_DATASET = {
+ "trailing_slash": {"data": "\\"},
+ "trailing_double_slash": {"data": "\\\\"},
+ "trailing_slash_after_text": {"data": "testtext\\"}}
+VALID_TXT_DATASET = {
+ "slash_with_one_trailing_space": {"data": "\"\\ \""},
+ "slash_with_many_trailing_space": {"data": "\"\\ \""},
+ "text_with_slash_and_trailing_space": {"data": "\"the txts \""}}
+INVALID_MX_DATASET = {
+ "empty_preference": {"pref": ""},
+ "minus_zero_preference": {"pref": "-0"},
+ "minus_one_preference": {"pref": "-1"}}
+INVALID_SSHFP_DATASET = {
+ "minus_zero_algorithm": {"algo": "-0", "finger": None},
+ "minus_zero_fingerprint": {"algo": None, "finger": "-0"},
+ "minus_one_algorithm": {"algo": "-1", "finger": None},
+ "minus_one_fingerprint": {"algo": None, "finger": "-1"}}
-@ddt.ddt
class RecordsetValidationTest(base.BaseDnsV2Test):
+ credentials = ["admin", "primary", "system_admin"]
+
def setUp(self):
super(RecordsetValidationTest, self).setUp()
self._zone = None
@@ -51,13 +73,24 @@
def setup_clients(cls):
super(RecordsetValidationTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
- cls.zones_client = cls.os_primary.dns_v2.ZonesClient()
@property
def zone(self):
if self._zone is None:
- zone_data = data_utils.rand_zone_data()
+ tld_name = dns_data_utils.rand_zone_name(
+ name="recordsetvalidation")
+ self.class_tld = self.admin_tld_client.create_tld(
+ tld_name=tld_name[:-1])
+ self.addCleanup(
+ self.admin_tld_client.delete_tld, self.class_tld[1]['id'])
+ zone_name = dns_data_utils.rand_zone_name(name="TestZone",
+ suffix=f'.{tld_name}')
+ zone_data = dns_data_utils.rand_zone_data(name=zone_name)
resp, body = self.zones_client.create_zone(**zone_data)
self._zone = body
self.addCleanup(self.wait_zone_delete,
@@ -72,40 +105,39 @@
return body
@decorators.idempotent_id('c5ef87e2-cb79-4758-b968-18eef2c251df')
- @ddt.data(*RECORDSETS_DATASET)
- def test_create_invalid(self, rtype):
- data = ["b0rk"]
-
- for i in data:
- model = data_utils.make_rand_recordset(self.zone['name'], rtype)
- model['data'] = i
-
- self.assertRaisesDns(
- exceptions.BadRequest, 'invalid_object', 400,
- self.recordset_client.create_recordset,
- self.zone['id'], model
- )
+ def test_create_invalid(self):
+ for rtype in RECORDSETS_DATASET:
+ data = ["b0rk"]
+ for i in data:
+ model = dns_data_utils.make_rand_recordset(
+ self.zone['name'], rtype)
+ model['data'] = i
+ self.assertRaisesDns(
+ exceptions.BadRequest, 'invalid_object', 400,
+ self.recordset_client.create_recordset,
+ self.zone['id'], model
+ )
@decorators.idempotent_id('1164c826-dceb-4557-9a22-7d65c4a4f5f4')
- @ddt.data(*RECORDSETS_DATASET)
- def test_update_invalid(self, rtype):
- data = ["b0rk"]
-
- post_model = data_utils.make_rand_recordset(self.zone['name'], rtype)
- recordset = self.create_recordset(post_model)
-
- for i in data:
- model = data_utils.make_rand_recordset(self.zone['name'], rtype)
- model['data'] = i
- self.assertRaisesDns(
- exceptions.BadRequest, 'invalid_object', 400,
- self.recordset_client.update_recordset,
- self.zone['id'], recordset['id'], model
- )
+ def test_update_invalid(self):
+ for rtype in RECORDSETS_DATASET:
+ data = ["b0rk"]
+ post_model = dns_data_utils.make_rand_recordset(
+ self.zone['name'], rtype)
+ recordset = self.create_recordset(post_model)
+ for i in data:
+ model = dns_data_utils.make_rand_recordset(
+ self.zone['name'], rtype)
+ model['data'] = i
+ self.assertRaisesDns(
+ exceptions.BadRequest, 'invalid_object', 400,
+ self.recordset_client.update_recordset,
+ self.zone['id'], recordset['id'], model
+ )
@decorators.idempotent_id('61da1015-291f-43d1-a1a8-345cff12d201')
def test_cannot_create_wildcard_NS_recordset(self):
- model = data_utils.wildcard_ns_recordset(self.zone['name'])
+ model = dns_data_utils.wildcard_ns_recordset(self.zone['name'])
self.assertRaisesDns(
exceptions.BadRequest, 'invalid_object', 400,
self.recordset_client.create_recordset, self.zone['id'], model
@@ -113,7 +145,7 @@
@decorators.idempotent_id('92f681aa-d953-4d18-b12e-81a9149ccfd9')
def test_cname_recordsets_cannot_have_more_than_one_record(self):
- post_model = data_utils.rand_cname_recordset(
+ post_model = dns_data_utils.rand_cname_recordset(
zone_name=self.zone['name'])
post_model['records'] = [
@@ -128,57 +160,68 @@
)
@decorators.idempotent_id('22a9544b-2382-4ed2-ba12-4dbaedb8e880')
- @ddt.file_data("invalid_txt_dataset.json")
- def test_cannot_create_TXT_with(self, data):
- post_model = data_utils.rand_txt_recordset(self.zone['name'], data)
- self.assertRaisesDns(
- exceptions.BadRequest, 'invalid_object', 400,
- self.recordset_client.create_recordset,
- self.zone['id'], post_model
- )
+ def test_cannot_create_TXT_with(self):
+ for key, data in INVALID_TXT_DATASET.items():
+ LOG.info('Tested INVALID_TXT_DATASET: {}'.format(key))
+ post_model = dns_data_utils.rand_txt_recordset(
+ self.zone['name'], data['data'])
+ self.assertRaisesDns(
+ exceptions.BadRequest, 'invalid_object', 400,
+ self.recordset_client.create_recordset,
+ self.zone['id'], post_model
+ )
@decorators.idempotent_id('03e4f811-0c37-4ce2-8b16-662c824f8f18')
- @ddt.file_data("valid_txt_dataset.json")
- def test_create_TXT_with(self, data):
- post_model = data_utils.rand_txt_recordset(self.zone['name'], data)
+ def test_create_TXT_with(self):
+ for key, data in VALID_TXT_DATASET.items():
+ LOG.info('Tested VALID_TXT_DATASET: {}'.format(key))
+ post_model = dns_data_utils.rand_txt_recordset(
+ self.zone['name'], data['data'])
recordset = self.create_recordset(post_model)
waiters.wait_for_recordset_status(
self.recordset_client, self.zone['id'], recordset['id'], 'ACTIVE')
@decorators.idempotent_id('775b3db5-ec60-4dd7-85d2-f05a9c544978')
- @ddt.file_data("valid_txt_dataset.json")
- def test_create_SPF_with(self, data):
- post_model = data_utils.rand_spf_recordset(self.zone['name'], data)
- recordset = self.create_recordset(post_model)
+ def test_create_SPF_with(self):
+ for key, data in VALID_TXT_DATASET.items():
+ LOG.info('Tested VALID_TXT_DATASET: {}'.format(key))
+ post_model = dns_data_utils.rand_spf_recordset(
+ self.zone['name'], data['data'])
+ recordset = self.create_recordset(post_model)
- waiters.wait_for_recordset_status(
- self.recordset_client, self.zone['id'], recordset['id'], 'ACTIVE')
+ waiters.wait_for_recordset_status(
+ self.recordset_client, self.zone['id'],
+ recordset['id'], 'ACTIVE')
@decorators.idempotent_id('7fa7783f-1624-4122-bfb2-6cfbf7a5b49b')
- @ddt.file_data("invalid_mx_dataset.json")
- def test_cannot_create_MX_with(self, pref):
- post_model = data_utils.rand_mx_recordset(
- self.zone['name'], pref=pref
- )
+ def test_cannot_create_MX_with(self):
+ for key, pref in INVALID_MX_DATASET.items():
+ LOG.info('Tested INVALID_MX_DATASET: {}'.format(key))
- self.assertRaisesDns(
- exceptions.BadRequest, 'invalid_object', 400,
- self.recordset_client.create_recordset,
- self.zone['id'], post_model,
- )
+ post_model = dns_data_utils.rand_mx_recordset(
+ self.zone['name'], pref=pref['pref']
+ )
+
+ self.assertRaisesDns(
+ exceptions.BadRequest, 'invalid_object', 400,
+ self.recordset_client.create_recordset,
+ self.zone['id'], post_model,
+ )
@decorators.idempotent_id('3016f998-4e4a-4712-b15a-4e8dfbc5a60b')
- @ddt.data("invalid_sshfp_dataset.json")
- def test_cannot_create_SSHFP_with(self, algo=None, finger=None):
- post_model = data_utils.rand_sshfp_recordset(
- zone_name=self.zone['name'],
- algorithm_number=algo,
- fingerprint_type=finger,
- )
+ def test_cannot_create_SSHFP_with(self):
+ for key, data in INVALID_SSHFP_DATASET.items():
+ LOG.info('Tested INVALID_SSHFP_DATASET: {}'.format(key))
- self.assertRaisesDns(
- exceptions.BadRequest, 'invalid_object', 400,
- self.recordset_client.create_recordset,
- self.zone['id'], post_model,
- )
+ post_model = dns_data_utils.rand_sshfp_recordset(
+ zone_name=self.zone['name'],
+ algorithm_number=data['algo'],
+ fingerprint_type=data['finger'],
+ )
+
+ self.assertRaisesDns(
+ exceptions.BadRequest, 'invalid_object', 400,
+ self.recordset_client.create_recordset,
+ self.zone['id'], post_model,
+ )
diff --git a/designate_tempest_plugin/tests/api/v2/test_service_statuses.py b/designate_tempest_plugin/tests/api/v2/test_service_statuses.py
index 0deeb74..0b63f21 100644
--- a/designate_tempest_plugin/tests/api/v2/test_service_statuses.py
+++ b/designate_tempest_plugin/tests/api/v2/test_service_statuses.py
@@ -24,27 +24,29 @@
LOG = logging.getLogger(__name__)
-class ServiceStatus(base.BaseDnsV2Test):
+class ServiceStatusAdmin(base.BaseDnsV2Test):
- credentials = ["primary", "admin", "system_admin", "alt"]
+ credentials = ["primary", "admin", "system_admin", "system_reader", "alt",
+ "project_reader", "project_member"]
+
+ mandatory_services = ['central', 'mdns', 'worker', 'producer']
+ service_status_fields = [
+ 'id', 'hostname', 'service_name', 'status', 'stats', 'capabilities',
+ 'heartbeated_at', 'created_at', 'updated_at', 'links']
@classmethod
def setup_credentials(cls):
# Do not create network resources for these test.
cls.set_network_resources()
- super(ServiceStatus, cls).setup_credentials()
+ super(ServiceStatusAdmin, cls).setup_credentials()
@classmethod
def setup_clients(cls):
- super(ServiceStatus, cls).setup_clients()
+ super(ServiceStatusAdmin, cls).setup_clients()
if CONF.enforce_scope.designate:
cls.admin_client = cls.os_system_admin.dns_v2.ServiceClient()
else:
cls.admin_client = cls.os_admin.dns_v2.ServiceClient()
- cls.client = cls.os_primary.dns_v2.ServiceClient()
-
- cls.primary_client = cls.os_primary.dns_v2.ServiceClient()
- cls.alt_client = cls.os_alt.dns_v2.ServiceClient()
@decorators.idempotent_id('bf277a76-8583-11eb-a557-74e5f9e2a801')
def test_admin_list_service_statuses(self):
@@ -57,8 +59,8 @@
LOG.info('Make sure that all expected/mandatory services are '
'listed in API response.')
- expected_services = ['central', 'mdns', 'worker', 'producer']
- for service in expected_services:
+
+ for service in self.mandatory_services:
self.assertIn(
service, [item[0] for item in services_statuses_tup],
"Failed, expected service: {} wasn't detected in API "
@@ -70,6 +72,46 @@
"Failed, not all listed services are in UP status, "
"services: {}".format(services_statuses_tup))
+ # Test RBAC
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin', 'os_system_reader']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'ServiceClient', 'list_statuses', expected_allowed, False)
+
+ @decorators.idempotent_id('fce0f704-c0ae-11ec-8213-201e8823901f')
+ def test_admin_show_service_status(self):
+
+ LOG.info('List services and get the IDs of mandatory services only')
+ services_ids = [
+ service['id'] for service in self.admin_client.list_statuses()
+ if service['service_name'] in self.mandatory_services]
+
+ LOG.info('Ensure all service status fields presents in response')
+ for id in services_ids:
+ service_show = self.admin_client.show_statuses(id)
+ self.assertEqual(
+ sorted(self.service_status_fields), sorted(service_show))
+
+
+class ServiceStatusNegative(base.BaseDnsV2Test):
+
+ credentials = ["primary", "alt"]
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(ServiceStatusNegative, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServiceStatusNegative, cls).setup_clients()
+ cls.primary_client = cls.os_primary.dns_v2.ServiceClient()
+ cls.alt_client = cls.os_alt.dns_v2.ServiceClient()
+
@decorators.idempotent_id('d4753f76-de43-11eb-91d1-74e5f9e2a801')
def test_primary_is_forbidden_to_list_service_statuses(self):
diff --git a/designate_tempest_plugin/tests/api/v2/test_shared_zones.py b/designate_tempest_plugin/tests/api/v2/test_shared_zones.py
new file mode 100644
index 0000000..78d6233
--- /dev/null
+++ b/designate_tempest_plugin/tests/api/v2/test_shared_zones.py
@@ -0,0 +1,564 @@
+# Copyright 2020 Cloudification GmbH. All rights reserved.
+#
+# 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 oslo_utils import uuidutils
+from oslo_utils import versionutils
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from designate_tempest_plugin.tests import base
+from designate_tempest_plugin import data_utils as dns_data_utils
+
+LOG = logging.getLogger(__name__)
+
+CONF = config.CONF
+
+
+class BaseSharedZoneTest(base.BaseDnsV2Test):
+
+ credentials = ['admin', 'system_admin', 'system_reader', 'primary', 'alt',
+ 'project_reader', 'project_member', ['demo', 'member']]
+
+ excluded_keys = ['links']
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseSharedZoneTest, cls).resource_setup()
+
+ if not versionutils.is_compatible('2.1', cls.api_version,
+ same_major=False):
+ raise cls.skipException(
+ 'The shared zones API tests require Designate API version '
+ '2.1 or newer. Skipping Shared Zones API tests.')
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="APISharedZoneTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ # All the shared zone tests need a zone, create one to share
+ zone_name = dns_data_utils.rand_zone_name(name="TestZone",
+ suffix=cls.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ cls.zone = cls.zones_client.create_zone(name=zone_name)[1]
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.zones_client.delete_zone(
+ cls.zone['id'], ignore_errors=lib_exc.NotFound, delete_shares=True)
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseSharedZoneTest, cls).resource_cleanup()
+
+ @classmethod
+ def setup_clients(cls):
+ super(BaseSharedZoneTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ cls.adm_shr_client = cls.os_system_admin.dns_v2.SharedZonesClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.adm_shr_client = cls.os_admin.dns_v2.SharedZonesClient()
+ cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
+ cls.demo_zone_client = cls.os_demo.dns_v2.ZonesClient()
+ cls.share_zone_client = cls.os_primary.dns_v2.SharedZonesClient()
+ cls.alt_share_zone_client = cls.os_alt.dns_v2.SharedZonesClient()
+
+
+class SharedZonesTest(BaseSharedZoneTest):
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(SharedZonesTest, cls).setup_credentials()
+
+ @decorators.idempotent_id('982a7780-a460-4c13-97df-b4855bf19c7b')
+ def test_create_zone_share(self):
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+ self.check_CUD_RBAC_enforcement(
+ 'SharedZonesClient', 'create_zone_share', expected_allowed, True,
+ self.zone['id'], self.alt_zone_client.project_id)
+
+ # Test a basic API create a zone share
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ self.assertTrue(uuidutils.is_uuid_like(shared_zone['id']))
+ self.assertEqual(self.zone['id'], shared_zone['zone_id'])
+ self.assertEqual(self.share_zone_client.project_id,
+ shared_zone['project_id'])
+ self.assertEqual(self.alt_zone_client.project_id,
+ shared_zone['target_project_id'])
+ self.assertIsNotNone(shared_zone['created_at'])
+ self.assertIsNone(shared_zone['updated_at'])
+ self.assertIsNotNone(shared_zone['links'])
+
+ @decorators.idempotent_id('0edecb9b-4890-433c-8195-0935271efc9a')
+ def test_show_shared_zone(self):
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+ expected_allowed.append('os_project_reader')
+ self.check_CUD_RBAC_enforcement(
+ 'SharedZonesClient', 'show_zone_share', expected_allowed, True,
+ self.zone['id'], shared_zone['id'])
+
+ # Test show zone share
+ LOG.info('Fetch the zone share')
+ body = self.share_zone_client.show_zone_share(self.zone['id'],
+ shared_zone['id'])[1]
+
+ LOG.info('Ensure the fetched response matches the zone share')
+ self.assertExpected(shared_zone, body, self.excluded_keys)
+
+ @decorators.idempotent_id('a18a8577-9d02-492a-a869-4ff7d6f4f89b')
+ def test_delete_zone_share(self):
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+ self.check_CUD_RBAC_enforcement(
+ 'SharedZonesClient', 'delete_zone_share', expected_allowed, True,
+ self.zone['id'], shared_zone['id'])
+
+ # Test zone share delete
+ LOG.info('Delete zone share')
+ self.share_zone_client.delete_zone_share(self.zone['id'],
+ shared_zone['id'])
+
+ LOG.info('Ensure the zone share was deleted')
+ self.assertRaises(lib_exc.NotFound,
+ self.share_zone_client.show_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ @decorators.idempotent_id('707bfa4f-f15b-4486-ba5c-0e5991f0f3a5')
+ def test_list_zone_shares(self):
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+ expected_allowed.append('os_project_reader')
+ self.check_CUD_RBAC_enforcement(
+ 'SharedZonesClient', 'list_zone_shares', expected_allowed, True,
+ self.zone['id'])
+
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.demo_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ LOG.info('List zone shares')
+ body = self.share_zone_client.list_zone_shares(self.zone['id'])[1]
+
+ self.assertEqual(2, len(body['shared_zones']))
+ targets = []
+ for share in body['shared_zones']:
+ targets.append(share['target_project_id'])
+ self.assertIn(self.alt_zone_client.project_id, targets)
+ self.assertIn(self.demo_zone_client.project_id, targets)
+
+
+class NegativeSharedZonesTest(BaseSharedZoneTest):
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(NegativeSharedZonesTest, cls).setup_credentials()
+
+ @decorators.idempotent_id('4389a12b-8609-493c-9640-d3c67b625022')
+ def test_target_project_cannot_delete_zone(self):
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ LOG.info('Ensure target project cannot delete zone')
+ self.assertRaises(lib_exc.Forbidden,
+ self.alt_zone_client.delete_zone,
+ self.zone['id'])
+
+ @decorators.idempotent_id('f4354b5c-8dbb-4bb9-8025-f65f8f2b21fb')
+ def test_target_project_cannot_update_zone(self):
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ LOG.info('Ensure target project cannot update the zone')
+ self.assertRaises(lib_exc.Forbidden,
+ self.alt_zone_client.update_zone,
+ self.zone['id'], ttl=5)
+
+ @decorators.idempotent_id('4389a12b-8609-493c-9640-d3c67b625022')
+ def test_target_project_share_permissions(self):
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ LOG.info('Ensure target project cannot share shared zone')
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.alt_share_zone_client.create_zone_share,
+ self.zone['id'],
+ self.demo_zone_client.project_id)
+
+ @decorators.idempotent_id('abc0f820-ae27-4e85-8f00-0b8e8abf3ae9')
+ def test_target_project_cannot_subzone(self):
+ shared_zone = self.share_zone_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'])
+
+ LOG.info('Ensure target project cannot create sub-zones')
+ sub_zone_name = "test.{}".format(self.zone['name'])
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.alt_zone_client.create_zone,
+ name=sub_zone_name)
+
+ @decorators.idempotent_id('957ba3f8-c250-11ed-a8b1-201e8823901f')
+ def test_share_zone_with_yourself_is_not_allowed(self):
+ with self.assertRaisesDns(lib_exc.BadRequest, 'bad_request', 400):
+ self.share_zone_client.create_zone_share(
+ zone_id=self.zone['id'],
+ target_project_id=self.share_zone_client.project_id)
+
+
+class AdminSharedZonesTest(BaseSharedZoneTest):
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(AdminSharedZonesTest, cls).setup_credentials()
+
+ @decorators.idempotent_id('2bb7bcb2-b824-11ed-9e56-201e8823901f')
+ def test_create_zone_share_all_projects_header(self):
+ LOG.info(
+ 'Admin user creates shared zone for Alt tenant '
+ 'using "x-auth-all-projects" header')
+ # Scoped tokens do not have a project ID, work around that here
+ if CONF.enforce_scope.designate:
+ headers = self.all_projects_header.copy()
+ headers.update(
+ {'x-auth-sudo-project-id': self.share_zone_client.project_id})
+ else:
+ headers = self.all_projects_header
+
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=headers)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=self.all_projects_header)
+ self.assertTrue(uuidutils.is_uuid_like(shared_zone['id']))
+ self.assertEqual(self.zone['id'], shared_zone['zone_id'])
+ if CONF.enforce_scope.designate:
+ self.assertEqual(self.share_zone_client.project_id,
+ shared_zone['project_id'])
+ else:
+ self.assertEqual(self.adm_shr_client.project_id,
+ shared_zone['project_id'])
+ self.assertEqual(self.alt_zone_client.project_id,
+ shared_zone['target_project_id'])
+ self.assertIsNotNone(shared_zone['created_at'])
+ self.assertIsNone(shared_zone['updated_at'])
+ self.assertIsNotNone(shared_zone['links'])
+
+ @decorators.idempotent_id('f26cd3ac-b8fa-11ed-b4ca-201e8823901f')
+ def test_create_zone_share_sudo_project_header(self):
+ LOG.info(
+ 'Admin user creates shared zone for Alt tenant '
+ 'using "x-auth-sudo-project-id" header')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.share_zone_client.project_id}
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=sudo_header)
+ self.assertTrue(uuidutils.is_uuid_like(shared_zone['id']))
+ self.assertEqual(self.zone['id'], shared_zone['zone_id'])
+ self.assertEqual(self.share_zone_client.project_id,
+ shared_zone['project_id'])
+ self.assertEqual(self.alt_zone_client.project_id,
+ shared_zone['target_project_id'])
+ self.assertIsNotNone(shared_zone['created_at'])
+ self.assertIsNone(shared_zone['updated_at'])
+ self.assertIsNotNone(shared_zone['links'])
+
+ @decorators.idempotent_id('ce2688e8-b90a-11ed-b4ca-201e8823901f')
+ def test_show_shared_zone_all_projects_header(self):
+ LOG.info(
+ 'Admin user creates shared zone for Alt tenant'
+ ' using "x-auth-all-projects" header')
+ # Scoped tokens do not have a project ID, work around that here
+ if CONF.enforce_scope.designate:
+ headers = self.all_projects_header.copy()
+ headers.update(
+ {'x-auth-sudo-project-id': self.share_zone_client.project_id})
+ else:
+ headers = self.all_projects_header
+
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=headers)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=self.all_projects_header)
+
+ LOG.info('Admin user shows shared zone and validates its content')
+ body = self.adm_shr_client.show_zone_share(
+ self.zone['id'], shared_zone['id'],
+ headers=self.all_projects_header)[1]
+ self.assertExpected(shared_zone, body, self.excluded_keys)
+
+ @decorators.idempotent_id('46f7db22-b90c-11ed-b4ca-201e8823901f')
+ def test_delete_zone_share_sudo_project_header(self):
+ LOG.info(
+ 'Admin user creates shared zone for Alt tenant'
+ ' using "x-auth-sudo-project-id" header')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.share_zone_client.project_id}
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=sudo_header,
+ ignore_errors=lib_exc.NotFound)
+
+ LOG.info('As Admin delete zone share and ensure it was deleted')
+ self.adm_shr_client.delete_zone_share(
+ self.zone['id'], shared_zone['id'], headers=sudo_header)
+ self.assertRaises(lib_exc.NotFound,
+ self.adm_shr_client.show_zone_share,
+ self.zone['id'], shared_zone['id'], headers=sudo_header)
+
+ @decorators.idempotent_id('2eedfd60-b90f-11ed-b4ca-201e8823901f')
+ def test_list_zone_shares_all_projects_header(self):
+ LOG.info(
+ "Admin user shares Primary's zone with Alt tenant"
+ " using 'x-auth-all-projects' header")
+ # Scoped tokens do not have a project ID, work around that here
+ if CONF.enforce_scope.designate:
+ headers = self.all_projects_header.copy()
+ headers.update(
+ {'x-auth-sudo-project-id': self.share_zone_client.project_id})
+ else:
+ headers = self.all_projects_header
+
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=headers)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=self.all_projects_header)
+
+ LOG.info(
+ "Admin user shares Primary's zone with Demo tenant"
+ " using 'x-auth-all-projects' header")
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.demo_zone_client.project_id,
+ headers=headers)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=self.all_projects_header)
+
+ LOG.info('Admin user lists zone shares')
+ body = self.adm_shr_client.list_zone_shares(
+ self.zone['id'], headers=self.all_projects_header)[1]
+
+ self.assertEqual(2, len(body['shared_zones']))
+ targets = []
+ for share in body['shared_zones']:
+ targets.append(share['target_project_id'])
+ self.assertIn(self.alt_zone_client.project_id, targets)
+ self.assertIn(self.demo_zone_client.project_id, targets)
+
+
+class AdminSharedZonesTestNegative(BaseSharedZoneTest):
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(AdminSharedZonesTestNegative, cls).setup_credentials()
+
+ @decorators.idempotent_id('595ae1fc-bce4-11ed-baf2-201e8823901f')
+ def test_create_zone_share_invalid_project_id(self):
+ LOG.info(
+ 'Admin user tries to create shared zone, using non existing '
+ 'project ID id in "x-auth-sudo-project-id" header')
+ sudo_header = {
+ 'x-auth-sudo-project-id': data_utils.rand_uuid()}
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.create_zone_share,
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=sudo_header)
+
+ @decorators.idempotent_id('aa42d82e-bcf6-11ed-baf2-201e8823901f')
+ def test_create_zone_share_invalid_zone_id(self):
+ LOG.info(
+ 'Admin user tries to create shared zone, using non existing '
+ 'zone ID and "x-auth-sudo-project-id" header')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.alt_zone_client.project_id}
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.create_zone_share,
+ data_utils.rand_uuid(), self.alt_zone_client.project_id,
+ headers=sudo_header)
+
+ @decorators.idempotent_id('9e7202ba-bd94-11ed-80f5-201e8823901f')
+ def test_show_shared_zone_invalid_shared_zone_id(self):
+ LOG.info('Admin tries to show shared zone using not '
+ 'existing shared zone ID')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.alt_zone_client.project_id}
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.show_zone_share,
+ self.zone['id'], data_utils.rand_uuid(), headers=sudo_header)
+
+ @decorators.idempotent_id('8852329c-bd95-11ed-80f5-201e8823901f')
+ def test_show_shared_zone_invalid_project_id(self):
+ LOG.info('Admin tries to show shared zone '
+ 'using not existing project ID')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.share_zone_client.project_id}
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=sudo_header)
+ sudo_header_invalid_project_id = {
+ 'x-auth-sudo-project-id': data_utils.rand_uuid()}
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.show_zone_share,
+ self.zone['id'], shared_zone['id'],
+ headers=sudo_header_invalid_project_id)
+
+ @decorators.idempotent_id('871e7e1c-bd9a-11ed-80f5-201e8823901f')
+ @decorators.skip_because(bug="2009819")
+ def test_list_zone_shares_invalid_zone_id(self):
+ LOG.info('Admin user tries to list shared zone '
+ 'using not existing zone ID')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.share_zone_client.project_id}
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.list_zone_shares,
+ data_utils.rand_uuid(), headers=sudo_header)
+
+ @decorators.idempotent_id('e71068c8-bdb1-11ed-80f5-201e8823901f')
+ @ decorators.skip_because(bug="2009819")
+ def test_list_zone_shares_invalid_project_id(self):
+ LOG.info('Admin user tries to list shared zone using '
+ 'not existing project ID')
+ sudo_header = {
+ 'x-auth-sudo-project-id': data_utils.rand_uuid()}
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.list_zone_shares,
+ self.zone['id'], headers=sudo_header)
+
+ @decorators.idempotent_id('7136b430-bdb2-11ed-80f5-201e8823901f')
+ def test_delete_zone_share_invalid_project_id(self):
+ LOG.info('Admin user creates shared zone for Alt user')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.share_zone_client.project_id}
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=sudo_header,
+ ignore_errors=lib_exc.NotFound)
+ LOG.info('Admin user tries to delete the shared zone '
+ 'using non existing project ID')
+ invalid_sudo_header = {
+ 'x-auth-sudo-project-id': data_utils.rand_uuid()}
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.delete_zone_share,
+ self.zone['id'], shared_zone['id'],
+ headers=invalid_sudo_header)
+
+ @decorators.idempotent_id('d44c65e2-bdc1-11ed-80f5-201e8823901f')
+ def test_delete_zone_share_invalid_shared_zone_id(self):
+ LOG.info('Admin user creates shared zone for Alt user')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.share_zone_client.project_id}
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=sudo_header,
+ ignore_errors=lib_exc.NotFound)
+ LOG.info('Admin user tries to delete the shared zone '
+ 'using non existing shared zone ID')
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.delete_zone_share,
+ self.zone['id'], data_utils.rand_uuid(),
+ headers=sudo_header)
+
+ @decorators.idempotent_id('06de2342-bdc2-11ed-80f5-201e8823901f')
+ def test_delete_zone_share_invalid_zone_id(self):
+ LOG.info('Admin user creates shared zone for Alt user')
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.share_zone_client.project_id}
+ shared_zone = self.adm_shr_client.create_zone_share(
+ self.zone['id'], self.alt_zone_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, self.zone['id'],
+ shared_zone['id'], headers=sudo_header,
+ ignore_errors=lib_exc.NotFound)
+ LOG.info('Admin user tries to delete the shared zone '
+ 'using non zone ID')
+ self.assertRaises(
+ lib_exc.NotFound, self.adm_shr_client.delete_zone_share,
+ data_utils.rand_uuid(), shared_zone['id'],
+ headers=sudo_header)
diff --git a/designate_tempest_plugin/tests/api/v2/test_tld.py b/designate_tempest_plugin/tests/api/v2/test_tld.py
index 831c13c..a74edc1 100644
--- a/designate_tempest_plugin/tests/api/v2/test_tld.py
+++ b/designate_tempest_plugin/tests/api/v2/test_tld.py
@@ -29,7 +29,11 @@
class TldAdminTest(BaseTldTest):
- credentials = ["admin", "system_admin", "primary"]
+ credentials = ["admin", "system_admin", "system_reader",
+ "primary", "alt", "project_reader", "project_member"]
+
+ # Use a TLD suffix unique to this test class.
+ local_tld_suffix = '.'.join(["tldadmintest", CONF.dns.tld_suffix])
@classmethod
def setup_credentials(cls):
@@ -45,43 +49,43 @@
else:
cls.admin_client = cls.os_admin.dns_v2.TldClient()
cls.primary_client = cls.os_primary.dns_v2.TldClient()
- cls.primary_zone_client = cls.os_primary.dns_v2.ZonesClient()
@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
- )
-
- @classmethod
- def resource_cleanup(cls):
- cls.admin_client.delete_tld(cls.tld[1]['id'])
- super(TldAdminTest, cls).resource_cleanup()
+ def _generate_tld_name(cls, test_name):
+ return '.'.join([data_utils.rand_name(name=test_name),
+ cls.local_tld_suffix])
@decorators.idempotent_id('52a4bb4b-4eff-4591-9dd3-ad98316806c3')
def test_create_tld(self):
- tld_data = {
- "name": "org",
- "description": "sample tld"}
+ tld_name = self._generate_tld_name("test_create_tld")
+ tld_data = {"name": tld_name, "description": "sample tld"}
LOG.info('Create a tld')
- _, tld = self.admin_client.create_tld(tld_data['name'],
- tld_data['description'])
+ tld = self.admin_client.create_tld(tld_data['name'],
+ tld_data['description'])[1]
self.addCleanup(self.admin_client.delete_tld, tld['id'])
- self.assertEqual(tld_data["name"], tld['name'])
+ self.assertEqual(tld_name, tld['name'])
+
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement('TldClient', 'create_tld',
+ expected_allowed, False)
@decorators.idempotent_id('961bd2e8-d4d0-11eb-b8ee-74e5f9e2a801')
def test_create_duplicated_tlds(self):
- tld_data = {
- "name": "org", "description": "test_create_duplicated_tlds"}
+ tld_name = self._generate_tld_name("test_create_duplicated_tlds")
+ tld_data = {"name": tld_name,
+ "description": "test_create_duplicated_tlds"}
LOG.info('Create a first "org" TLD')
tld = self.admin_client.create_tld(
tld_data['name'], tld_data['description'])[1]
self.addCleanup(self.admin_client.delete_tld, tld['id'])
- self.assertEqual(tld_data["name"], tld['name'])
+ self.assertEqual(tld_name, tld['name'])
LOG.info('Try to create a second "org" TLD')
self.assertRaises(
@@ -91,15 +95,15 @@
@decorators.idempotent_id('0c0ab92e-d4db-11eb-b8ee-74e5f9e2a801')
def test_create_multiply_tlds(self):
- tlds = ['abc', 'def', 'gih']
- for tld_name in tlds:
- tld_data = {
- "name": tld_name, "description": "test_create_multiply_tlds"}
+ for _dummy in range(0, 2):
+ tld_name = self._generate_tld_name("test_create_multiply_tlds")
+ tld_data = {"name": tld_name,
+ "description": "test_create_multiply_tlds"}
LOG.info('Create a "{}" TLD'.format(tld_name))
tld = self.admin_client.create_tld(
tld_data['name'], tld_data['description'])[1]
self.addCleanup(self.admin_client.delete_tld, tld['id'])
- self.assertEqual(tld_data["name"], tld['name'])
+ self.assertEqual(tld_name, tld['name'])
@decorators.idempotent_id('52a4bb4b-4eff-4591-9dd3-ad98316806c3')
def test_create_invalid_tld(self):
@@ -116,21 +120,6 @@
lib_exc.BadRequest, self.admin_client.create_tld,
tld_name='org', description='test_create_invalid_tld' * 1000)
- @decorators.idempotent_id('06deced8-d4de-11eb-b8ee-74e5f9e2a801')
- def test_create_zone_for_not_existing_tld(self):
- LOG.info('Create an "org" TLD')
- tld_data = {"name": "org",
- "description": "test_create_zone_for_not_existing_tld"}
- tld = self.admin_client.create_tld(
- tld_data['name'], tld_data['description'])[1]
- self.addCleanup(self.admin_client.delete_tld, tld['id'])
- self.assertEqual(tld_data["name"], tld['name'])
-
- LOG.info('Try to create a Primary zone with "zzz" (not existing) TLD.')
- self.assertRaises(
- lib_exc.BadRequest, self.primary_zone_client.create_zone,
- name='example.zzz.')
-
@decorators.idempotent_id('757019c0-d4e2-11eb-b8ee-74e5f9e2a801')
def test_create_tld_as_primary_user(self):
tld_data = {
@@ -142,58 +131,104 @@
@decorators.idempotent_id('271af08c-2603-4f61-8eb1-05887b74e25a')
def test_show_tld(self):
+ tld_name = self._generate_tld_name("test_show_tld")
tld_data = {
- "name": "org",
+ "name": tld_name,
"description": "sample tld"}
LOG.info('Create a tld')
- _, tld = self.admin_client.create_tld(tld_data['name'],
- tld_data['description'])
+ tld = self.admin_client.create_tld(tld_data['name'],
+ tld_data['description'])[1]
self.addCleanup(self.admin_client.delete_tld, tld['id'])
LOG.info('Fetch the tld')
- _, body = self.admin_client.show_tld(tld['id'])
+ body = self.admin_client.show_tld(tld['id'])[1]
LOG.info('Ensure the fetched response matches the created tld')
self.assertExpected(tld, body, self.excluded_keys)
+ # Test RBAC
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin', 'os_system_reader']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'TldClient', 'show_tld', expected_allowed, False, tld['id'])
+
@decorators.idempotent_id('26708cb8-7126-48a7-9424-1c225e56e609')
def test_delete_tld(self):
LOG.info('Create a tld')
- _, tld = self.admin_client.create_tld()
+ tld_name = self._generate_tld_name("test_delete_tld")
+ tld = self.admin_client.create_tld(tld_name)[1]
self.addCleanup(self.admin_client.delete_tld, tld['id'],
ignore_errors=lib_exc.NotFound)
LOG.info('Delete the tld')
- _, body = self.admin_client.delete_tld(tld['id'])
+ self.admin_client.delete_tld(tld['id'])
- self.assertRaises(lib_exc.NotFound,
- lambda: self.admin_client.show_tld(tld['id']))
+ self.assertRaises(lib_exc.NotFound, self.admin_client.show_tld,
+ tld['id'])
+
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement('TldClient', 'delete_tld',
+ expected_allowed, False, tld['id'])
@decorators.idempotent_id('95b13759-c85c-4791-829b-9591ca15779d')
def test_list_tlds(self):
LOG.info('List tlds')
- _, body = self.admin_client.list_tlds()
+ tld_name = self._generate_tld_name("test_list_tlds")
+ tld = self.admin_client.create_tld(tld_name)[1]
+ self.addCleanup(self.admin_client.delete_tld, tld['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ body = self.admin_client.list_tlds()[1]
self.assertGreater(len(body['tlds']), 0)
+ # Test RBAC
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'TldClient', 'list_tlds', expected_allowed, [tld['id']],
+ params={'limit': 1000})
+
@decorators.idempotent_id('1a233812-48d9-4d15-af5e-9961744286ff')
def test_update_tld(self):
- _, tld = self.admin_client.create_tld()
+ tld_name = self._generate_tld_name("test_update_tld")
+ tld = self.admin_client.create_tld(tld_name)[1]
self.addCleanup(self.admin_client.delete_tld, tld['id'])
+ tld_name_2 = self._generate_tld_name("test_update_tld")
+
tld_data = {
- "name": "org",
+ "name": tld_name_2,
"description": "Updated description"
}
LOG.info('Update the tld')
- _, patch_tld = self.admin_client.update_tld(tld['id'],
- tld_data['name'], tld_data['description'])
+ patch_tld = self.admin_client.update_tld(tld['id'],
+ tld_data['name'], tld_data['description'])[1]
- self.assertEqual(tld_data["name"], patch_tld["name"])
+ self.assertEqual(tld_name_2, patch_tld["name"])
self.assertEqual(tld_data["description"], patch_tld["description"])
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TldClient', 'update_tld', expected_allowed, False, tld['id'],
+ tld_data['name'], tld_data['description'])
+
@decorators.idempotent_id('8116dcf5-a329-47d1-90be-5ff32f299c53')
def test_list_tlds_dot_json_fails(self):
uri = self.admin_client.get_uri('tlds.json')
@@ -204,7 +239,7 @@
class TestTldNotFoundAdmin(BaseTldTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
@@ -250,7 +285,7 @@
class TestTldInvalidIdAdmin(BaseTldTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
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 616431c..94c661a 100644
--- a/designate_tempest_plugin/tests/api/v2/test_transfer_accepts.py
+++ b/designate_tempest_plugin/tests/api/v2/test_transfer_accepts.py
@@ -18,6 +18,7 @@
from tempest.lib import exceptions as lib_exc
from designate_tempest_plugin.tests import base
+from designate_tempest_plugin import data_utils as dns_data_utils
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -27,9 +28,33 @@
excluded_keys = ['created_at', 'updated_at', 'key', 'links',
'zone_name']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseTransferAcceptTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseTransferAcceptTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseTransferAcceptTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseTransferAcceptTest, cls).resource_cleanup()
+
class TransferAcceptTest(BaseTransferAcceptTest):
- credentials = ["primary", "alt", "admin", "system_admin"]
+ credentials = ["primary", "alt", "admin", "system_admin", "system_reader",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -42,7 +67,6 @@
super(TransferAcceptTest, cls).setup_clients()
# Primary clients
- cls.prm_zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.prm_request_client = cls.os_primary.dns_v2.TransferRequestClient()
cls.prm_accept_client = cls.os_primary.dns_v2.TransferAcceptClient()
@@ -68,7 +92,10 @@
@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(wait_until='ACTIVE')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_transfer_accept", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
self.addCleanup(
self.wait_zone_delete, self.admin_zone_client, zone['id'],
headers=self.all_projects_header,
@@ -87,6 +114,24 @@
"key": transfer_request['key'],
"zone_transfer_request_id": transfer_request['id']
}
+
+ # Test RBAC
+ # Note: Everyone can call this API and succeed if they know the
+ # transfer key.
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ # Note: system_reader is allowed because this API RBAC is based
+ # on the target project ID. It will return a 401 instead of
+ # a 403.
+ expected_allowed.append('os_system_reader')
+ expected_allowed.append('os_project_member')
+ expected_allowed.append('os_project_reader')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TransferAcceptClient', 'create_transfer_accept',
+ expected_allowed, True, data)
+
LOG.info('Create a zone transfer_accept')
_, transfer_accept = self.prm_accept_client.create_transfer_accept(
data)
@@ -97,7 +142,10 @@
@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(wait_until='ACTIVE')
+ zone_name = dns_data_utils.rand_zone_name(name="show_transfer_accept",
+ suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
self.addCleanup(
self.wait_zone_delete, self.admin_zone_client, zone['id'],
headers=self.all_projects_header,
@@ -129,11 +177,34 @@
'created transfer_accept')
self.assertExpected(transfer_accept, body, self.excluded_keys)
+ # Test RBAC
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_member',
+ 'os_project_reader'])
+
+ self.check_list_show_RBAC_enforcement(
+ 'TransferAcceptClient', 'show_transfer_accept', expected_allowed,
+ True, transfer_accept['id'])
+
+ # Test RBAC with x-auth-all-projects
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'TransferAcceptClient', 'show_transfer_accept', expected_allowed,
+ True, transfer_accept['id'], headers=self.all_projects_header)
+
@decorators.idempotent_id('89b516f0-8c9f-11eb-a322-74e5f9e2a801')
def test_ownership_transferred_zone(self):
LOG.info('Create a Primary zone')
- zone = self.prm_zone_client.create_zone(wait_until='ACTIVE')[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="ownership_transferred_zone", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
self.addCleanup(
self.wait_zone_delete, self.admin_zone_client, zone['id'],
headers=self.all_projects_header,
@@ -178,7 +249,10 @@
for _ in range(number_of_zones_to_transfer):
LOG.info('Create a Primary zone')
- zone = self.prm_zone_client.create_zone(wait_until='ACTIVE')[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_transfer_accepts", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
self.addCleanup(
self.wait_zone_delete, self.admin_zone_client, zone['id'],
headers=self.all_projects_header,
@@ -205,6 +279,28 @@
self.assertEqual('COMPLETE', transfer_accept['status'])
transfer_request_ids.append(transfer_accept['id'])
+ # Test RBAC - Users that are allowed to call list, but should get
+ # zero zones.
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_RBAC_enforcement_count(
+ 'TransferAcceptClient', 'list_transfer_accept',
+ expected_allowed, 0)
+
+ # Test that users who should see the zone, can see it.
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'TransferAcceptClient', 'list_transfer_accept',
+ expected_allowed, transfer_request_ids,
+ headers=self.all_projects_header)
+
# 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
@@ -214,7 +310,8 @@
admin_client_accept_ids = [
item['id'] for item in
self.admin_accept_client.list_transfer_accept(
- headers=self.all_projects_header, params={'limit': 1000})]
+ headers=self.all_projects_header,
+ params={'limit': 1000})[1]['transfer_accepts']]
for tr_id in transfer_request_ids:
self.assertIn(
tr_id, admin_client_accept_ids,
@@ -229,7 +326,7 @@
item['id'] for item in
self.admin_accept_client.list_transfer_accept(
headers=self.all_projects_header,
- params={'status': 'COMPLETE'})]
+ params={'status': 'COMPLETE'})[1]['transfer_accepts']]
for tr_id in transfer_request_ids:
self.assertIn(
tr_id, admin_client_accept_ids,
@@ -246,7 +343,7 @@
item['id'] for item in
self.admin_accept_client.list_transfer_accept(
headers=self.all_projects_header,
- params={'status': not_existing_status})]
+ params={'status': not_existing_status})[1]['transfer_accepts']]
self.assertEmpty(
admin_client_accept_ids,
"Failed, filtered list should be empty, but actually it's not, "
@@ -255,7 +352,10 @@
@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]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_transfer_accept_impersonate", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ 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
@@ -302,6 +402,18 @@
self.addCleanup(
self.wait_zone_delete, self.alt_zone_client, zone['id'])
+ # Test RBAC with x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'TransferAcceptClient', 'show_transfer_accept', expected_allowed,
+ True, transfer_accept['id'],
+ headers={'x-auth-sudo-project-id':
+ self.os_alt.credentials.project_id})
+
class TransferAcceptTestNegative(BaseTransferAcceptTest):
@@ -316,15 +428,17 @@
@classmethod
def setup_clients(cls):
super(TransferAcceptTestNegative, cls).setup_clients()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.request_client = cls.os_primary.dns_v2.TransferRequestClient()
cls.client = cls.os_primary.dns_v2.TransferAcceptClient()
@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'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_transfer_accept_invalid_key", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone transfer_request')
transfer_request = self.request_client.create_transfer_request(
@@ -335,7 +449,8 @@
transfer_request['id']
)
- data = {"key": data_utils.rand_password(len(transfer_request['key'])),
+ 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')
@@ -346,8 +461,11 @@
@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'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_transfer_accept_deleted_id", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone transfer_request')
transfer_request = self.request_client.create_transfer_request(
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 6c8ed07..20c68ed 100644
--- a/designate_tempest_plugin/tests/api/v2/test_transfer_request.py
+++ b/designate_tempest_plugin/tests/api/v2/test_transfer_request.py
@@ -27,9 +27,34 @@
class BaseTransferRequestTest(base.BaseDnsV2Test):
excluded_keys = ['created_at', 'updated_at', 'key', 'links']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseTransferRequestTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseTransferRequestTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(
+ name="BaseTransferRequestTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseTransferRequestTest, cls).resource_cleanup()
+
class TransferRequestTest(BaseTransferRequestTest):
- credentials = ["primary", "alt", "admin", "system_admin"]
+ credentials = ["primary", "alt", "admin", "system_admin", "system_reader",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -46,7 +71,6 @@
TransferRequestClient())
else:
cls.admin_client = cls.os_admin.dns_v2.TransferRequestClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
cls.client = cls.os_primary.dns_v2.TransferRequestClient()
cls.alt_client = cls.os_alt.dns_v2.TransferRequestClient()
@@ -54,11 +78,23 @@
@decorators.idempotent_id('2381d489-ad84-403d-b0a2-8b77e4e966bf')
def test_create_transfer_request(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_transfer_request", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TransferRequestClient', 'create_transfer_request',
+ expected_allowed, True, zone['id'])
LOG.info('Create a zone transfer_request')
- _, transfer_request = self.client.create_transfer_request(zone['id'])
+ transfer_request = self.client.create_transfer_request(zone['id'])[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'])
@@ -68,15 +104,17 @@
@decorators.idempotent_id('5deae1ac-7c14-42dc-b14e-4e4b2725beb7')
def test_create_transfer_request_scoped(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_transfer_request_scoped", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
transfer_request_data = dns_data_utils.rand_transfer_request_data(
target_project_id=self.os_alt.credentials.project_id)
LOG.info('Create a scoped zone transfer_request')
- _, transfer_request = self.client.create_transfer_request(
- zone['id'], transfer_request_data)
+ transfer_request = self.client.create_transfer_request(
+ zone['id'], transfer_request_data)[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'])
@@ -86,11 +124,13 @@
@decorators.idempotent_id('4505152f-0a9c-4f02-b385-2216c914a0be')
def test_create_transfer_request_empty_body(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_transfer_request_empty", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone transfer_request')
- _, transfer_request = self.client.create_transfer_request_empty_body(
- zone['id'])
+ transfer_request = self.client.create_transfer_request_empty_body(
+ zone['id'])[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'])
@@ -100,27 +140,58 @@
@decorators.idempotent_id('64a7be9f-8371-4ce1-a242-c1190de7c985')
def test_show_transfer_request(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_transfer_request", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone transfer_request')
- _, transfer_request = self.client.create_transfer_request(zone['id'])
+ transfer_request = self.client.create_transfer_request(zone['id'])[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'])
LOG.info('Fetch the transfer_request')
- _, body = self.client.show_transfer_request(transfer_request['id'])
+ body = self.client.show_transfer_request(transfer_request['id'])[1]
LOG.info('Ensure the fetched response matches the '
'created transfer_request')
self.assertExpected(transfer_request, body, self.excluded_keys)
+ # Test RBAC
+ # Note: The create service client does not define a target project
+ # ID, so everyone should be able to see it.
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt',
+ 'os_system_admin', 'os_system_reader',
+ 'os_project_member', 'os_project_reader']
+
+ self.check_list_show_RBAC_enforcement(
+ 'TransferRequestClient', 'show_transfer_request', expected_allowed,
+ True, transfer_request['id'])
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'TransferRequestClient', 'show_transfer_request', expected_allowed,
+ True, transfer_request['id'], headers=self.all_projects_header)
+ # TODO(johnsom) Move this down to the impersonate test below when the
+ # bug is resolved and the test is not skipped.
+ self.check_list_show_RBAC_enforcement(
+ 'TransferRequestClient', 'show_transfer_request', expected_allowed,
+ True, transfer_request['id'],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('5bed4582-9cfb-11eb-a160-74e5f9e2a801')
@decorators.skip_because(bug="1926572")
def test_show_transfer_request_impersonate_another_project(self):
LOG.info('Create a zone')
- zone = self.zone_client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_transfer_request_impersonate", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone transfer_request')
transfer_request = self.client.create_transfer_request(zone['id'])[1]
@@ -148,20 +219,22 @@
# Checks the target of a scoped transfer request can see
# the request.
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_transfer_request_as_target", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
transfer_request_data = dns_data_utils.rand_transfer_request_data(
target_project_id=self.os_alt.credentials.project_id)
LOG.info('Create a scoped zone transfer_request')
- _, transfer_request = self.client.create_transfer_request(
- zone['id'], transfer_request_data)
+ transfer_request = self.client.create_transfer_request(
+ zone['id'], transfer_request_data)[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'])
LOG.info('Fetch the transfer_request as the target')
- _, body = self.alt_client.show_transfer_request(transfer_request['id'])
+ body = self.alt_client.show_transfer_request(transfer_request['id'])[1]
LOG.info('Ensure the fetched response matches the '
'created transfer_request')
@@ -169,48 +242,97 @@
"project_id"]
self.assertExpected(transfer_request, body, excluded_keys)
+ # Test RBAC when a transfer target project is specified.
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_primary', 'os_alt',
+ 'os_system_admin', 'os_project_member']
+ else:
+ expected_allowed = ['os_primary', 'os_alt', 'os_admin',
+ 'os_system_admin', 'os_project_member']
+
+ self.check_list_show_RBAC_enforcement(
+ 'TransferRequestClient', 'show_transfer_request', expected_allowed,
+ True, transfer_request['id'])
+
@decorators.idempotent_id('7d81c487-aa15-44c4-b3e5-424ab9e6a3e5')
def test_delete_transfer_request(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="delete_transfer_request", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a transfer_request')
- _, transfer_request = self.client.create_transfer_request(zone['id'])
+ transfer_request = self.client.create_transfer_request(zone['id'])[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'],
ignore_errors=lib_exc.NotFound)
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TransferRequestClient', 'delete_transfer_request',
+ expected_allowed, True, transfer_request['id'])
+
LOG.info('Delete the transfer_request')
- _, body = self.client.delete_transfer_request(transfer_request['id'])
+ self.client.delete_transfer_request(transfer_request['id'])
self.assertRaises(lib_exc.NotFound,
lambda: self.client.show_transfer_request(transfer_request['id']))
@decorators.idempotent_id('ddd42a19-1768-428c-846e-32f9d6493011')
def test_list_transfer_requests(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_transfer_request", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone transfer_request')
- _, transfer_request = self.client.create_transfer_request(zone['id'])
+ transfer_request = self.client.create_transfer_request(zone['id'])[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'])
LOG.info('List transfer_requests')
- _, body = self.client.list_transfer_requests()
+ body = self.client.list_transfer_requests()[1]
self.assertGreater(len(body['transfer_requests']), 0)
+ # Test RBAC - Users that are allowed to call list, but should get
+ # zero zones.
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin', 'os_admin']
+ else:
+ expected_allowed = ['os_alt']
+
+ self.check_list_RBAC_enforcement_count(
+ 'TransferRequestClient', 'list_transfer_requests',
+ expected_allowed, 0)
+
+ # Test that users who should see the zone, can see it.
+ expected_allowed = ['os_primary']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'TransferRequestClient', 'list_transfer_requests',
+ expected_allowed, [transfer_request['id']])
+
@decorators.idempotent_id('db985892-9d02-11eb-a160-74e5f9e2a801')
def test_list_transfer_requests_all_projects(self):
LOG.info('Create a Primary zone')
- primary_zone = self.zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_transfer_request_all_projects", suffix=self.tld_name)
+ primary_zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(self.wait_zone_delete,
- self.zone_client, primary_zone['id'])
+ self.zones_client, primary_zone['id'])
LOG.info('Create an Alt zone')
- alt_zone = self.alt_zone_client.create_zone()[1]
+ alt_zone_name = dns_data_utils.rand_zone_name(
+ name="list_transfer_request_all_projects_alt",
+ suffix=self.tld_name)
+ alt_zone = self.alt_zone_client.create_zone(name=alt_zone_name)[1]
self.addCleanup(self.wait_zone_delete,
self.alt_zone_client, alt_zone['id'])
@@ -248,15 +370,30 @@
"Failed, transfer request ID:{} wasn't found in "
"listed IDs{}".format(request_id, request_ids))
+ # Test RBAC with x-auth-all-projects
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'TransferRequestClient', 'list_transfer_requests',
+ expected_allowed, [primary_transfer_request['id']],
+ headers=self.all_projects_header)
+
@decorators.idempotent_id('bee42f38-e666-4b85-a710-01f40ea1e56a')
def test_list_transfer_requests_impersonate_another_project(self):
LOG.info('Create a Primary zone')
- primary_zone = self.zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_transfer_request_impersonate", suffix=self.tld_name)
+ primary_zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(self.wait_zone_delete,
- self.zone_client, primary_zone['id'])
+ self.zones_client, primary_zone['id'])
LOG.info('Create an Alt zone')
- alt_zone = self.alt_zone_client.create_zone()[1]
+ alt_zone_name = dns_data_utils.rand_zone_name(
+ name="list_transfer_request_impersonate_alt", suffix=self.tld_name)
+ alt_zone = self.alt_zone_client.create_zone(name=alt_zone_name)[1]
self.addCleanup(self.wait_zone_delete,
self.alt_zone_client, alt_zone['id'])
@@ -279,27 +416,66 @@
self.assertEqual([alt_transfer_request['id']], request_ids)
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'TransferRequestClient', 'list_transfer_requests',
+ expected_allowed, [primary_transfer_request['id']],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('de5e9d32-c723-4518-84e5-58da9722cc13')
def test_update_transfer_request(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="update_transfer_request", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone transfer_request')
- _, transfer_request = self.client.create_transfer_request(zone['id'])
+ transfer_request = self.client.create_transfer_request(zone['id'])[1]
self.addCleanup(self.client.delete_transfer_request,
transfer_request['id'])
LOG.info('Update the transfer_request')
data = {
- "description": "demo descripion"
+ "description": "demo description"
}
- _, transfer_request_patch = self.client.update_transfer_request(
- transfer_request['id'], transfer_request_data=data)
+ transfer_request_patch = self.client.update_transfer_request(
+ transfer_request['id'], transfer_request_data=data)[1]
self.assertEqual(data['description'],
transfer_request_patch['description'])
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'TransferRequestClient', 'update_transfer_request',
+ expected_allowed, True,
+ transfer_request['id'], transfer_request_data=data)
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TransferRequestClient', 'update_transfer_request',
+ expected_allowed, False,
+ transfer_request['id'], transfer_request_data=data,
+ headers=self.all_projects_header)
+ self.check_CUD_RBAC_enforcement(
+ 'TransferRequestClient', 'update_transfer_request',
+ expected_allowed, False,
+ transfer_request['id'], transfer_request_data=data,
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('73b754a9-e856-4fd6-80ba-e8d1b80f5dfa')
def test_list_transfer_requests_dot_json_fails(self):
uri = self.client.get_uri('transfer_requests.json')
@@ -309,6 +485,7 @@
class TestTransferRequestNotFound(BaseTransferRequestTest):
+ credentials = ["admin", "primary", "system_admin"]
@classmethod
def setup_credentials(cls):
@@ -321,6 +498,15 @@
super(TestTransferRequestNotFound, cls).setup_clients()
cls.client = cls.os_primary.dns_v2.TransferRequestClient()
+ @decorators.idempotent_id('39131f7c-e9bb-4f92-a325-444a675e1b3d')
+ def test_create_transfer_request_404(self):
+ e = self.assertRaises(lib_exc.NotFound,
+ self.client.create_transfer_request,
+ data_utils.rand_uuid())
+ self.assertEqual(404, e.resp.status)
+ self.assertEqual(404, e.resp_body['code'])
+ self.assertEqual("zone_not_found", e.resp_body['type'])
+
@decorators.idempotent_id('d255f72f-ba24-43df-9dba-011ed7f4625d')
def test_show_transfer_request_404(self):
e = self.assertRaises(lib_exc.NotFound,
@@ -346,11 +532,10 @@
self.assertEqual(404, resp.status)
self.assertEqual(404, resp_body['code'])
self.assertEqual("zone_transfer_request_not_found", resp_body['type'])
- self.assertEqual("Could not find ZoneTransferRequest",
- resp_body['message'])
class TestTransferRequestInvalidId(BaseTransferRequestTest):
+ credentials = ["admin", "primary", "system_admin"]
@classmethod
def setup_credentials(cls):
diff --git a/designate_tempest_plugin/tests/api/v2/test_tsigkey.py b/designate_tempest_plugin/tests/api/v2/test_tsigkey.py
index 292b821..926797f 100644
--- a/designate_tempest_plugin/tests/api/v2/test_tsigkey.py
+++ b/designate_tempest_plugin/tests/api/v2/test_tsigkey.py
@@ -19,6 +19,7 @@
from tempest.lib import exceptions as lib_exc
from designate_tempest_plugin.tests import base
+from designate_tempest_plugin import data_utils as dns_data_utils
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -27,9 +28,33 @@
class BaseTsigkeyTest(base.BaseDnsV2Test):
excluded_keys = ['created_at', 'updated_at', 'links']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseTsigkeyTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseTsigkeyTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseTsigkeyTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseTsigkeyTest, cls).resource_cleanup()
+
class TsigkeyAdminTest(BaseTsigkeyTest):
- credentials = ["primary", "admin", "system_admin"]
+ credentials = ["primary", "admin", "system_admin", "system_reader",
+ "project_member", "project_reader", "alt"]
@classmethod
def setup_credentials(cls):
@@ -42,95 +67,415 @@
super(TsigkeyAdminTest, cls).setup_clients()
if CONF.enforce_scope.designate:
cls.admin_client = cls.os_system_admin.dns_v2.TsigkeyClient()
+ cls.pool_admin_client = cls.os_system_admin.dns_v2.PoolClient()
else:
cls.admin_client = cls.os_admin.dns_v2.TsigkeyClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
+ cls.pool_admin_client = cls.os_admin.dns_v2.PoolClient()
+
+ cls.primary_client = cls.os_primary.dns_v2.TsigkeyClient()
@decorators.idempotent_id('e7b484e3-7ed5-4840-89d7-1e696986f8e4')
- def test_create_tsigkey(self):
+ def test_create_tsigkey_for_zone(self):
LOG.info('Create a resource')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_tsigkey_for_zone", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
tsigkey_data = {
- "name": "Example tsigkey",
+ "name": dns_data_utils.rand_zone_name(
+ 'test_create_tsigkey_for_zone'),
"algorithm": "hmac-sha256",
"secret": "SomeSecretKey",
- "scope": "POOL",
+ "scope": "ZONE",
"resource_id": zone['id']}
LOG.info('Create a tsigkey')
- _, tsigkey = self.admin_client.create_tsigkey(
+ tsigkey = self.admin_client.create_tsigkey(
tsigkey_data['resource_id'],
tsigkey_data['name'], tsigkey_data['algorithm'],
- tsigkey_data['secret'], tsigkey_data['scope'])
+ tsigkey_data['secret'], tsigkey_data['scope'])[1]
self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
self.assertEqual(tsigkey_data["name"], tsigkey['name'])
+ self.assertEqual(tsigkey_data["scope"], 'ZONE')
+
+ @decorators.idempotent_id('45975fa6-d726-11eb-beba-74e5f9e2a801')
+ def test_create_tsigkey_for_pool(self):
+ LOG.info('Get the valid pool ID from list of pools')
+ pool = self.pool_admin_client.list_pools()[1]['pools'][0]
+
+ LOG.info('Create a tsigkey')
+ tsigkey_data = {
+ "name": dns_data_utils.rand_zone_name('Example_Key'),
+ "algorithm": "hmac-sha256",
+ "secret": "SomeSecretKey",
+ "scope": "POOL",
+ "resource_id": pool['id']}
+ tsigkey = self.admin_client.create_tsigkey(
+ tsigkey_data['resource_id'],
+ tsigkey_data['name'], tsigkey_data['algorithm'],
+ tsigkey_data['secret'], tsigkey_data['scope'])[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
+ self.assertEqual(tsigkey_data["name"], tsigkey['name'])
+ self.assertEqual(tsigkey_data["scope"], 'POOL')
+
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TsigkeyClient', 'create_tsigkey', expected_allowed, False,
+ tsigkey_data['resource_id'],
+ tsigkey_data['name'], tsigkey_data['algorithm'],
+ tsigkey_data['secret'], tsigkey_data['scope'])
@decorators.idempotent_id('d46e5e86-a18c-4315-aa0c-95a00e816fbf')
def test_list_tsigkey(self):
LOG.info('Create a resource')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_tsigkey", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a tsigkey')
- _, tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])
+ tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])[1]
self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
- _, body = self.admin_client.list_tsigkeys()
+ body = self.admin_client.list_tsigkeys()[1]
self.assertGreater(len(body['tsigkeys']), 0)
+ # Test RBAC
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'TsigkeyClient', 'list_tsigkeys', expected_allowed,
+ [tsigkey['id']])
+
+ @decorators.idempotent_id('d46e5e86-a18c-4315-aa0c-95a00e816fbf')
+ def test_list_tsigkeys_limit_results(self):
+ for i in range(3):
+ LOG.info('As Primary user create a zone: {} '.format(i))
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_tsigkey_limit", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(
+ self.wait_zone_delete, self.zones_client, zone['id'])
+ LOG.info('As Admin user create a tsigkey: {} '.format(i))
+ tsigkey = self.admin_client.create_tsigkey(
+ resource_id=zone['id'])[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
+ LOG.info('As Admin client, list all tsigkey using '
+ 'URL query: "limit=2"')
+ body = self.admin_client.list_tsigkeys(params={'limit': 2})[1]
+ self.assertEqual(len(body['tsigkeys']), 2)
+
+ @decorators.idempotent_id('f31447b0-d817-11eb-b95a-74e5f9e2a801')
+ def test_list_tsigkeys_using_marker(self):
+ test_tsigkeys_name = 'marker_tsigkey_'
+ test_tsigkeys_names = [test_tsigkeys_name + str(i) for i in range(4)]
+
+ LOG.info('Create tsigkeys named: {}'.format(test_tsigkeys_names))
+ created_tsigkeys = []
+ for name in test_tsigkeys_names:
+ LOG.info('As Primary user create a zone to be used '
+ 'for {}'.format(name))
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_tsigkey_marker", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(
+ self.wait_zone_delete, self.zones_client, zone['id'])
+ LOG.info('As Admin user create "{}" tsigkey'.format(name))
+ tsigkey = self.admin_client.create_tsigkey(
+ resource_id=zone['id'], name=name)[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
+ created_tsigkeys.append(tsigkey['id'])
+
+ LOG.info('As Admin, list all tsigkeys using url queries:"limit=2" '
+ 'and "name={}*"'.format(test_tsigkeys_name))
+ body = self.admin_client.list_tsigkeys(
+ params={
+ 'limit': 2, 'name': test_tsigkeys_name + '*'})[1]
+ tsigkeys = body['tsigkeys']
+ self.assertEqual(2, len(tsigkeys),
+ 'Failed, response is not limited as expected')
+
+ LOG.info('Get the marker to be used for subsequent request')
+ first_set_of_ids = [item['id'] for item in tsigkeys]
+ links = body['links']
+ marker = links['next'].split('marker=')[-1]
+
+ LOG.info('Use marker for subsequent request to get the rest of '
+ 'tsigkeys that contains "{}*" in their '
+ 'names'.format(test_tsigkeys_name))
+ tsigkeys = self.admin_client.list_tsigkeys(
+ params={'marker': marker, 'limit': 2,
+ 'name': test_tsigkeys_name + '*'})[1]['tsigkeys']
+ self.assertEqual(2, len(tsigkeys),
+ 'Failed, response is not limited as expected')
+ second_set_of_ids = [item['id'] for item in tsigkeys]
+
+ LOG.info('Make sure that the merge of tsigkeys IDs received in two '
+ 'phases using "marker" url query, contains all the IDs '
+ 'created within the test')
+ self.assertEqual(
+ sorted(first_set_of_ids + second_set_of_ids),
+ sorted(created_tsigkeys),
+ 'Failed, tsigkeys IDs received in two phases are not as expected')
+
+ @decorators.idempotent_id('d5c6dfcc-d8af-11eb-b95a-74e5f9e2a801')
+ def test_list_tsigkey_sort_key_with_sort_direction(self):
+ names_to_create = [data_utils.rand_name(name) for name in
+ ['bbb_tsgikey', 'aaa_tsgikey', 'ccc_tsgikey']]
+ created_tsigkey_ids = []
+ for name in names_to_create:
+ LOG.info('As Primary user create a zone for: {} '.format(name))
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_tsigkey_sort", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(
+ self.wait_zone_delete, self.zones_client, zone['id'])
+ LOG.info('As Admin user create a tsigkey: {} '.format(name))
+ tsigkey = self.admin_client.create_tsigkey(
+ resource_id=zone['id'], name=name)[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
+ created_tsigkey_ids.append(tsigkey['id'])
+
+ LOG.info('As Admin, list all tsigkeys using "asc" to sort by names')
+ sorted_tsigkeys = self.admin_client.list_tsigkeys(
+ params={'sort_dir': 'asc', 'sort_key': 'name'})[1]['tsigkeys']
+ sorted_by_names = [item['name'] for item in sorted_tsigkeys]
+ self.assertEqual(
+ sorted(sorted_by_names),
+ sorted_by_names,
+ 'Failed, tsgikeys names are not sorted in "asc" as expected')
+
+ LOG.info('As Admin, list all tsigkey using "desc" to sort by names')
+ sorted_tsigkeys = self.admin_client.list_tsigkeys(
+ params={'sort_dir': 'desc', 'sort_key': 'name'})[1]['tsigkeys']
+ sorted_by_names = [item['name'] for item in sorted_tsigkeys]
+ self.assertEqual(
+ sorted(sorted_by_names, reverse=True),
+ sorted_by_names,
+ 'Failed, tsgikeys names are not sorted in "desc" as expected')
+
+ LOG.info('As Admin, list all tsigkeys using "asc" to sort by ID')
+ sorted_tsigkeys = self.admin_client.list_tsigkeys(
+ params={'sort_dir': 'asc', 'sort_key': 'id'})[1]['tsigkeys']
+ sorted_by_ids = [item['id'] for item in sorted_tsigkeys]
+ self.assertEqual(
+ sorted(sorted_by_ids),
+ sorted_by_ids,
+ 'Failed, tsgikeys IDs are not sorted in "asc" as expected')
+
+ LOG.info('As Admin, list all tsigkeys using "zababun" direction '
+ 'to sort by names, expected: "invalid_sort_dir"')
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_sort_dir', 400,
+ self.admin_client.list_tsigkeys,
+ params={'sort_dir': 'zababun', 'sort_key': 'name'})
+
+ LOG.info('As Admin, list all tsigkeys using "zababun" as a key value,'
+ 'expected: "invalid_sort_key"')
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_sort_key', 400,
+ self.admin_client.list_tsigkeys,
+ params={'sort_dir': 'asc', 'sort_key': 'zababun'})
+
+ @decorators.idempotent_id('4162a840-d8b2-11eb-b95a-74e5f9e2a801')
+ def test_list_tsigkey_filter_by_name(self):
+ tsigkey_name = data_utils.rand_name('ddd_tsgikey')
+ LOG.info('As Primary user create a zone for: {} '.format(tsigkey_name))
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_tsigkey_filter_name", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ LOG.info('As Admin user create a tsigkey: {} '.format(tsigkey_name))
+ tsigkey = self.admin_client.create_tsigkey(
+ resource_id=zone['id'], name=tsigkey_name)[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
+
+ LOG.info('As Admin, list all tsigkeys named:{}'.format(tsigkey_name))
+ listed_tsigkeys = self.admin_client.list_tsigkeys(
+ params={'name': tsigkey_name})[1]['tsigkeys']
+ self.assertEqual(
+ 1, len(listed_tsigkeys),
+ 'Failed, only a single tsigkey, named: {} should be '
+ 'listed.'.format(tsigkey_name))
+
+ LOG.info('As Admin, list all tsigkeys named:"zababun"')
+ listed_tsigkeys = self.admin_client.list_tsigkeys(
+ params={'name': 'zababun'})[1]['tsigkeys']
+ self.assertEqual(
+ 0, len(listed_tsigkeys), 'Failed, no tsigkey should be listed')
+
+ @decorators.idempotent_id('e8bcf80a-d8b4-11eb-b95a-74e5f9e2a801')
+ def test_list_tsigkey_filter_by_scope(self):
+
+ LOG.info('Create tsigkey for a pool')
+ pool = self.pool_admin_client.create_pool(
+ project_id=self.os_admin.credentials.project_id)[1]
+ self.addCleanup(
+ self.pool_admin_client.delete_pool, pool['id'],
+ headers={
+ 'x-auth-sudo-project-id':
+ self.os_admin.credentials.project_id})
+ pool_tsigkey = self.admin_client.create_tsigkey(
+ resource_id=pool['id'], scope='POOL')[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, pool_tsigkey['id'])
+
+ LOG.info('Create tsigkey for a zone')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_tsigkey_filter_scope", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ zone_tsigkey = self.admin_client.create_tsigkey(
+ resource_id=zone['id'], scope='ZONE')[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, zone_tsigkey['id'])
+
+ LOG.info('List all "scope=POOL" tsigkeys')
+ listed_pool_scopes = [
+ item['scope'] for item in self.admin_client.list_tsigkeys(
+ params={'scope': 'POOL'})[1]['tsigkeys']]
+ self.assertEqual(
+ {'POOL'}, set(listed_pool_scopes),
+ 'Failed, the only scopes expected to be listed are: "POOL"')
+
+ LOG.info('List all "scope=ZONE" tsigkeys')
+ listed_zone_scopes = [
+ item['scope'] for item in self.admin_client.list_tsigkeys(
+ params={'scope': 'ZONE'})[1]['tsigkeys']]
+ self.assertEqual(
+ {'ZONE'}, set(listed_zone_scopes),
+ 'Failed, the only scopes expected to be listed are: "ZONE"')
+
+ LOG.info('List all "scope=zababun" tsigkeys')
+ listed_zone_scopes = [
+ item['scope'] for item in self.admin_client.list_tsigkeys(
+ params={'scope': 'zababun'})[1]['tsigkeys']]
+ self.assertEqual(
+ 0, len(listed_zone_scopes),
+ 'Failed, no tsigkey is expected to be listed')
+
+ @decorators.idempotent_id('794554f0-d8b8-11eb-b95a-74e5f9e2a801')
+ def test_list_tsigkey_filter_by_algorithm(self):
+
+ LOG.info('Create tsigkey for a pool')
+ algorithm = 'hmac-sha256'
+ pool = self.pool_admin_client.create_pool(
+ project_id=self.os_admin.credentials.project_id)[1]
+ self.addCleanup(
+ self.pool_admin_client.delete_pool, pool['id'],
+ headers={
+ 'x-auth-sudo-project-id':
+ self.os_admin.credentials.project_id})
+ pool_tsigkey = self.admin_client.create_tsigkey(
+ resource_id=pool['id'], algorithm=algorithm)[1]
+ self.addCleanup(self.admin_client.delete_tsigkey, pool_tsigkey['id'])
+
+ LOG.info('List all "algorithm={}" tsigkeys '.format(algorithm))
+ listed_tsigkeys = [
+ item['algorithm'] for item in self.admin_client.list_tsigkeys(
+ params={'algorithm': algorithm})[1]['tsigkeys']]
+ self.assertEqual(
+ {algorithm}, set(listed_tsigkeys),
+ 'Failed, the only tsigkeys expected to be listed must '
+ 'have algorithm:{} '.format(algorithm))
+
+ LOG.info('List all "algorithm=zababun" tsigkeys')
+ listed_tsigkeys = [
+ item['algorithm'] for item in self.admin_client.list_tsigkeys(
+ params={'algorithm': 'zababun'})[1]['tsigkeys']]
+ self.assertEqual(
+ 0, len(listed_tsigkeys),
+ "Failed, no tsigkey is expectedto be listed")
+
@decorators.idempotent_id('c5d7facf-0f05-47a2-a4fb-87f203860880')
def test_show_tsigkey(self):
LOG.info('Create a resource')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_tsigkey", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a tsigkey')
- _, tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])
+ tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])[1]
self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
LOG.info('Fetch the tsigkey')
- _, body = self.admin_client.show_tsigkey(tsigkey['id'])
+ body = self.admin_client.show_tsigkey(tsigkey['id'])[1]
LOG.info('Ensure the fetched response matches the created tsigkey')
self.assertExpected(tsigkey, body, self.excluded_keys)
+ # Test RBAC
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin', 'os_system_reader']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'TsigkeyClient', 'show_tsigkey', expected_allowed, True,
+ tsigkey['id'])
+
@decorators.idempotent_id('d09dc0dd-dd72-41ee-9085-2afb2bf35459')
def test_update_tsigkey(self):
LOG.info('Create a resource')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="update_tsigkey", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a tsigkey')
- _, tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])
+ tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])[1]
self.addCleanup(self.admin_client.delete_tsigkey, tsigkey['id'])
tsigkey_data = {
"name": "Patch tsigkey",
- "secret": "NewSecretKey",
- "scope": "POOL"}
+ "secret": "NewSecretKey"}
LOG.info('Update the tsigkey')
- _, patch_tsigkey = self.admin_client.update_tsigkey(tsigkey['id'],
+ patch_tsigkey = self.admin_client.update_tsigkey(tsigkey['id'],
name=tsigkey_data['name'],
- secret=tsigkey_data['secret'],
- scope=tsigkey_data['scope'])
+ secret=tsigkey_data['secret'])[1]
self.assertEqual(tsigkey_data['name'], patch_tsigkey['name'])
self.assertEqual(tsigkey_data['secret'], patch_tsigkey['secret'])
- self.assertEqual(tsigkey_data['scope'], patch_tsigkey['scope'])
+
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TsigkeyClient', 'update_tsigkey', expected_allowed, False,
+ tsigkey['id'], name=tsigkey_data['name'],
+ secret=tsigkey_data['secret'])
@decorators.idempotent_id('9cdffbd2-bc67-4a25-8eb7-4be8635c88a3')
def test_delete_tsigkey(self):
LOG.info('Create a resource')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="delete_tsigkey", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a tsigkey')
- _, tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])
+ tsigkey = self.admin_client.create_tsigkey(resource_id=zone['id'])[1]
+
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'TsigkeyClient', 'delete_tsigkey', expected_allowed, False,
+ tsigkey['id'])
LOG.info('Delete the tsigkey')
- _, body = self.admin_client.delete_tsigkey(tsigkey['id'])
+ self.admin_client.delete_tsigkey(tsigkey['id'])
self.assertRaises(lib_exc.NotFound,
lambda: self.admin_client.show_tsigkey(tsigkey['id']))
@@ -145,7 +490,7 @@
class TestTsigkeyNotFoundAdmin(BaseTsigkeyTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
@@ -191,7 +536,7 @@
class TestTsigkeyInvalidIdAdmin(BaseTsigkeyTest):
- credentials = ["admin", "system_admin"]
+ credentials = ["admin", "primary", "system_admin"]
@classmethod
def setup_credentials(cls):
@@ -204,8 +549,10 @@
super(TestTsigkeyInvalidIdAdmin, cls).setup_clients()
if CONF.enforce_scope.designate:
cls.admin_client = cls.os_system_admin.dns_v2.TsigkeyClient()
+ cls.pool_admin_client = cls.os_system_admin.dns_v2.PoolClient()
else:
cls.admin_client = cls.os_admin.dns_v2.TsigkeyClient()
+ cls.pool_admin_client = cls.os_admin.dns_v2.PoolClient()
@decorators.idempotent_id('2a8dfc75-9884-4b1c-8f1f-ed835d96f2fe')
def test_show_tsigkey_invalid_uuid(self):
@@ -234,3 +581,117 @@
self.assertEqual("invalid_uuid", resp_body['type'])
self.assertEqual("Invalid UUID tsigkey_id: foo",
resp_body['message'])
+
+ @decorators.idempotent_id('f94af13a-d743-11eb-beba-74e5f9e2a801')
+ def test_create_tsigkey_for_zone_invalid_algorithm(self):
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_tsigkey_invalid_algo", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ tsigkey_data = {
+ "name": dns_data_utils.rand_zone_name('Example_Key'),
+ "algorithm": "zababun",
+ "secret": "SomeSecretKey",
+ "scope": "ZONE",
+ "resource_id": zone['id']}
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_object', 400,
+ self.admin_client.create_tsigkey,
+ tsigkey_data['resource_id'],
+ tsigkey_data['name'], tsigkey_data['algorithm'],
+ tsigkey_data['secret'], tsigkey_data['scope'])
+
+ @decorators.idempotent_id('4df903d8-d745-11eb-beba-74e5f9e2a801')
+ def test_create_tsigkey_for_zone_invalid_name(self):
+ LOG.info('Create a zone resource')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_tsigkey_invalid_name", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ tsigkey_data = {
+ "name": dns_data_utils.rand_zone_name(
+ 'Example_Key') * 1000,
+ "algorithm": "hmac-sha256",
+ "secret": "SomeSecretKey",
+ "scope": "ZONE",
+ "resource_id": zone['id']}
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_object', 400,
+ self.admin_client.create_tsigkey,
+ tsigkey_data['resource_id'],
+ tsigkey_data['name'], tsigkey_data['algorithm'],
+ tsigkey_data['secret'], tsigkey_data['scope'])
+
+ @decorators.idempotent_id('5d6b8a84-d745-11eb-beba-74e5f9e2a801')
+ @decorators.skip_because(bug="1933760")
+ def test_create_tsigkey_for_zone_empty_secret(self):
+ LOG.info('Create a zone resource')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_tsigkey_empty_secret", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ tsigkey_data = {
+ "name": dns_data_utils.rand_zone_name('Example_Key'),
+ "algorithm": "hmac-sha256",
+ "secret": '',
+ "scope": "ZONE",
+ "resource_id": zone['id']}
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_object', 400,
+ self.admin_client.create_tsigkey,
+ tsigkey_data['resource_id'],
+ tsigkey_data['name'], tsigkey_data['algorithm'],
+ tsigkey_data['secret'], tsigkey_data['scope'])
+
+ @decorators.idempotent_id('dfca9268-d745-11eb-beba-74e5f9e2a801')
+ def test_create_tsigkey_for_zone_invalid_scope(self):
+ LOG.info('Create a zone resource')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_tsigkey_invalid_scope", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ tsigkey_data = {
+ "name": dns_data_utils.rand_zone_name('Example_Key'),
+ "algorithm": "hmac-sha256",
+ "secret": "SomeSecretKey",
+ "scope": "zababun",
+ "resource_id": zone['id']}
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_object', 400,
+ self.admin_client.create_tsigkey,
+ tsigkey_data['resource_id'],
+ tsigkey_data['name'], tsigkey_data['algorithm'],
+ tsigkey_data['secret'], tsigkey_data['scope'])
+
+ @decorators.idempotent_id('57255858-d74a-11eb-beba-74e5f9e2a801')
+ def test_create_tsigkey_for_zone_invalid_zone_id(self):
+ LOG.info('Create a resource')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_tsigkey_invalide_zone_id", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ tsigkey_data = {
+ "name": dns_data_utils.rand_zone_name('Example_Key'),
+ "algorithm": "hmac-sha256",
+ "secret": "SomeSecretKey",
+ "scope": "ZONE",
+ "resource_id": data_utils.rand_uuid}
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_object', 400,
+ self.admin_client.create_tsigkey,
+ tsigkey_data['resource_id'],
+ tsigkey_data['name'], tsigkey_data['algorithm'],
+ tsigkey_data['secret'], tsigkey_data['scope'])
+
+ @decorators.idempotent_id('0dfbc2f8-d8bb-11eb-b95a-74e5f9e2a801')
+ @decorators.skip_because(bug="1934120")
+ def test_create_tsigkey_for_pool_with_scope_zone(self):
+ pool = self.pool_admin_client.create_pool()[1]
+ self.addCleanup(self.pool_admin_client.delete_pool, pool['id'])
+
+ LOG.info('Try to create a tsigkey using pool ID and "scope:ZONE", '
+ 'should fail because ID is for pool, but scope is ZONE')
+ self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_object', 400,
+ self.admin_client.create_tsigkey,
+ resource_id=pool['id'], scope='ZONE')
diff --git a/designate_tempest_plugin/tests/api/v2/test_unauthed.py b/designate_tempest_plugin/tests/api/v2/test_unauthed.py
index aaf8043..b151a04 100644
--- a/designate_tempest_plugin/tests/api/v2/test_unauthed.py
+++ b/designate_tempest_plugin/tests/api/v2/test_unauthed.py
@@ -14,7 +14,6 @@
from oslo_log import log as logging
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
-import ddt
from designate_tempest_plugin.tests import base
from designate_tempest_plugin import clients
@@ -22,7 +21,6 @@
LOG = logging.getLogger(__name__)
-@ddt.ddt
class TestDnsUnauthed(base.BaseDnsV2Test):
client_manager = clients.ManagerV2Unauthed
@@ -43,10 +41,154 @@
cls.pool_client = cls.os_primary.pool_client
cls.blacklists_client = cls.os_primary.blacklists_client
- @decorators.idempotent_id('0f7a6d20-f6f3-4937-8fe6-7a9851227d98')
- @ddt.file_data('unauthed_data.json')
- def test_unauthed(self, client, method, args=None):
+ def _test_unauthed(self, client, method, args=None):
client = getattr(self, client)
method = getattr(client, method)
args = args or []
self.assertRaises(lib_exc.Unauthorized, method, *args)
+
+ @decorators.idempotent_id('b18827ac-de92-11ed-8334-201e8823901f')
+ def test_list_zones(self):
+ self._test_unauthed('zones_client', 'list_zones')
+
+ @decorators.idempotent_id('f60c32ce-de92-11ed-8334-201e8823901f')
+ def test_show_zone(self):
+ self._test_unauthed(
+ 'zones_client', 'show_zone',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('56e899c0-de93-11ed-8334-201e8823901f')
+ def test_create_zone(self):
+ self._test_unauthed(
+ 'zones_client', 'create_zone',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('5765af6e-de93-11ed-8334-201e8823901f')
+ def test_update_zone(self):
+ self._test_unauthed(
+ 'zones_client', 'update_zone',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('57b5cef4-de93-11ed-8334-201e8823901f')
+ def test_delete_zone(self):
+ self._test_unauthed(
+ 'zones_client', 'delete_zone',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('05099b62-de94-11ed-8334-201e8823901f')
+ def test_list_recordsets(self):
+ self._test_unauthed(
+ 'recordset_client', 'list_recordset',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('0573ca32-de94-11ed-8334-201e8823901f')
+ def test_show_recordset(self):
+ self._test_unauthed(
+ 'recordset_client', 'show_recordset',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90",
+ "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('05c0236e-de94-11ed-8334-201e8823901f')
+ def test_create_recordset(self):
+ self._test_unauthed(
+ 'recordset_client', 'create_recordset',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90",
+ "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('0600f628-de94-11ed-8334-201e8823901f')
+ def test_update_recordset(self):
+ self._test_unauthed(
+ 'recordset_client', 'update_recordset',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90",
+ "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90", {}])
+
+ @decorators.idempotent_id('063c95b6-de94-11ed-8334-201e8823901f')
+ def test_delete_recordset(self):
+ self._test_unauthed(
+ 'recordset_client', 'delete_recordset',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90",
+ "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('ee9dae6c-de94-11ed-8334-201e8823901f')
+ def test_list_tlds(self):
+ self._test_unauthed('tld_client', 'list_tlds')
+
+ @decorators.idempotent_id('eef1e5f4-de94-11ed-8334-201e8823901f')
+ def test_show_tld(self):
+ self._test_unauthed(
+ 'tld_client', 'show_tld',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('ef3ae024-de94-11ed-8334-201e8823901f')
+ def test_create_tld(self):
+ self._test_unauthed(
+ 'tld_client', 'create_tld',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('ef7cfda6-de94-11ed-8334-201e8823901f')
+ def test_update_tld(self):
+ self._test_unauthed(
+ 'tld_client', 'update_tld',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('efb982e4-de94-11ed-8334-201e8823901f')
+ def test_delete_tld(self):
+ self._test_unauthed(
+ 'tld_client', 'delete_tld',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('654e7596-de95-11ed-8334-201e8823901f')
+ def test_list_blacklists(self):
+ self._test_unauthed('blacklists_client', 'list_blacklists')
+
+ @decorators.idempotent_id('658ea9cc-de95-11ed-8334-201e8823901f')
+ def test_show_blacklist(self):
+ self._test_unauthed(
+ 'blacklists_client', 'show_blacklist',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('65cbc2ee-de95-11ed-8334-201e8823901f')
+ def test_create_blacklist(self):
+ self._test_unauthed(
+ 'blacklists_client', 'create_blacklist',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('66032676-de95-11ed-8334-201e8823901f')
+ def test_update_blacklist(self):
+ self._test_unauthed(
+ 'blacklists_client', 'update_blacklist',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('66321184-de95-11ed-8334-201e8823901f')
+ def test_delete_blacklist(self):
+ self._test_unauthed(
+ 'blacklists_client', 'delete_blacklist',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('c7048d66-de95-11ed-8334-201e8823901f')
+ def test_list_pools(self):
+ self._test_unauthed('pool_client', 'list_pools')
+
+ @decorators.idempotent_id('c74581cc-de95-11ed-8334-201e8823901f')
+ def test_show_pool(self):
+ self._test_unauthed(
+ 'pool_client', 'show_pool',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('c77d62f4-de95-11ed-8334-201e8823901f')
+ def test_create_pool(self):
+ self._test_unauthed(
+ 'pool_client', 'create_pool',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('c7ada040-de95-11ed-8334-201e8823901f')
+ def test_update_pool(self):
+ self._test_unauthed(
+ 'pool_client', 'update_pool',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
+
+ @decorators.idempotent_id('c7e07682-de95-11ed-8334-201e8823901f')
+ def test_delete_pool(self):
+ self._test_unauthed(
+ 'pool_client', 'delete_pool',
+ ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"])
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 bbeea75..4523c53 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zone_tasks.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zone_tasks.py
@@ -13,19 +13,19 @@
# under the License.
from socket import gaierror
-from unittest import expectedFailure
from oslo_log import log as logging
from tempest import config
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
+import testtools
-from designate_tempest_plugin.common import constants as const
from designate_tempest_plugin.common import waiters
+from designate_tempest_plugin import data_utils as dns_data_utils
from designate_tempest_plugin.tests import base
-from designate_tempest_plugin.services.dns.query.query_client \
- import SingleQueryClient
+from designate_tempest_plugin.services.dns.query.query_client import (
+ SingleQueryClient)
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -35,9 +35,33 @@
excluded_keys = ['created_at', 'updated_at', 'version', 'links',
'status', 'action']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseZonesTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseZonesTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseZonesTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseZonesTest, cls).resource_cleanup()
+
class ZoneTasks(BaseZonesTest):
- credentials = ["primary", "alt", "admin", "system_admin"]
+ credentials = ["primary", "alt", "admin", "system_admin", "system_reader",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -52,52 +76,76 @@
cls.admin_client = cls.os_system_admin.dns_v2.ZonesClient()
else:
cls.admin_client = cls.os_admin.dns_v2.ZonesClient()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_client = cls.os_alt.dns_v2.ZonesClient()
@decorators.idempotent_id('287e2cd0-a0e7-11eb-b962-74e5f9e2a801')
+ @testtools.skipUnless(
+ config.CONF.dns.nameservers,
+ "Config option dns.nameservers is missing or empty")
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')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="zone_abandon", suffix=self.tld_name)
+ pr_zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client,
+ pr_zone['id'])
+ waiters.wait_for_zone_status(self.zones_client, pr_zone['id'],
+ 'ACTIVE')
LOG.info('Ensure we respond with CREATE+PENDING')
self.assertEqual('CREATE', pr_zone['action'])
self.assertEqual('PENDING', pr_zone['status'])
LOG.info('Fetch the zone')
- self.client.show_zone(pr_zone['id'])
+ self.zones_client.show_zone(pr_zone['id'])
LOG.info('Check that the zone was created on Nameserver/BIND')
waiters.wait_for_query(self.query_client, pr_zone['name'], "SOA")
+ # Test RBAC
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZonesClient', 'abandon_zone', expected_allowed, False,
+ pr_zone['id'],
+ headers={'x-auth-sudo-project-id': pr_zone['project_id']})
+
+ # Test abandoning the zone
LOG.info('Abandon a zone')
self.admin_client.abandon_zone(
pr_zone['id'],
headers={'x-auth-sudo-project-id': pr_zone['project_id']})
LOG.info('Wait for the zone to become 404/NotFound in Designate')
- waiters.wait_for_zone_404(self.client, pr_zone['id'])
+ waiters.wait_for_zone_404(self.zones_client, pr_zone['id'])
LOG.info('Check that the zone is still exists in Nameserver/BIND')
waiters.wait_for_query(
self.query_client, pr_zone['name'], "SOA")
@decorators.idempotent_id('90b21d1a-a1ba-11eb-84fa-74e5f9e2a801')
+ @testtools.skipUnless(
+ config.CONF.dns.nameservers,
+ "Config option dns.nameservers is missing or empty")
def test_zone_abandon_forbidden(self):
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')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="zone_abandon_forbidden", suffix=self.tld_name)
+ pr_zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client,
+ pr_zone['id'])
+ waiters.wait_for_zone_status(self.zones_client, pr_zone['id'],
+ 'ACTIVE')
LOG.info('Ensure we respond with CREATE+PENDING')
self.assertEqual('CREATE', pr_zone['action'])
self.assertEqual('PENDING', pr_zone['status'])
LOG.info('Fetch the zone')
- self.client.show_zone(pr_zone['id'])
+ self.zones_client.show_zone(pr_zone['id'])
LOG.info('Check that the zone was created on Nameserver/BIND')
waiters.wait_for_query(self.query_client, pr_zone['name'], "SOA")
@@ -105,7 +153,7 @@
LOG.info('Abandon a zone as primary client, Expected: should '
'fail with: 403 forbidden')
self.assertRaises(
- lib_exc.Forbidden, self.client.abandon_zone,
+ lib_exc.Forbidden, self.zones_client.abandon_zone,
zone_id=pr_zone['id'])
@@ -125,7 +173,6 @@
cls.admin_client = cls.os_system_admin.dns_v2.ZonesClient()
else:
cls.admin_client = cls.os_admin.dns_v2.ZonesClient()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_client = cls.os_alt.dns_v2.ZonesClient()
def _query_nameserver(self, nameserver, query_timeout,
@@ -138,47 +185,3 @@
except gaierror as e:
LOG.info('Function "_query_nameserver" failed with:{} '.format(e))
return query_succeeded
-
- @expectedFailure
- @decorators.idempotent_id('ca250d92-8a2b-11eb-b49b-74e5f9e2a801')
- def test_manually_trigger_update_secondary_zone_negative(self):
- # Create a PRIMARY zone
- 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'])
- self.assertEqual('PENDING', pr_zone['status'])
-
- # Get the Name Servers created for a PRIMARY zone
- nameservers = [
- dic['hostname'] for dic in self.client.show_zone_nameservers(
- pr_zone['id'])[1]['nameservers']]
-
- # Make sure that the nameservers are not available using DNS
- # query and if it does, skip the test.
- LOG.info('Check if NameServers are available, skip the test if not')
- for ns in nameservers:
- if self._query_nameserver(
- ns, 5, pr_zone['name'], zone_type='SOA') is True:
- raise self.skipException(
- "Nameserver:{} is available, but negative test scenario "
- "needs it to be unavailable, therefore test is "
- "skipped.".format(ns.strip('.')))
-
- # Create a SECONDARY zone
- LOG.info('Create a SECONDARY zone')
- sec_zone = self.client.create_zone(
- zone_type=const.SECONDARY_ZONE_TYPE, primaries=nameservers)[1]
- self.addCleanup(self.wait_zone_delete, self.client, sec_zone['id'])
- LOG.info('Ensure we respond with CREATE+PENDING')
- self.assertEqual('CREATE', sec_zone['action'])
- self.assertEqual('PENDING', sec_zone['status'])
-
- # Manually trigger_update zone
- LOG.info('Manually Trigger an Update of a Secondary Zone when the '
- 'nameservers not pingable. Expected: error status code 500')
- with self.assertRaisesDns(lib_exc.ServerFault, 'unknown', 500):
- self.client.trigger_manual_update(sec_zone['id'])
diff --git a/designate_tempest_plugin/tests/api/v2/test_zones.py b/designate_tempest_plugin/tests/api/v2/test_zones.py
index dec5028..d971790 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones.py
@@ -11,8 +11,8 @@
# 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 uuid
from oslo_log import log as logging
+from oslo_utils import versionutils
from tempest import config
from tempest.lib import decorators
from tempest.lib.common.utils import data_utils
@@ -20,12 +20,9 @@
from designate_tempest_plugin.common import constants as const
-
from designate_tempest_plugin import data_utils as dns_data_utils
from designate_tempest_plugin.tests import base
-from designate_tempest_plugin.common import waiters
-
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -34,9 +31,31 @@
excluded_keys = ['created_at', 'updated_at', 'version', 'links',
'status', 'action']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseZonesTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseZonesTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseZonesTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseZonesTest, cls).resource_cleanup()
+
class ZonesTest(BaseZonesTest):
- credentials = ["admin", "system_admin", "primary"]
@classmethod
def setup_credentials(cls):
@@ -51,47 +70,71 @@
cls.pool_client = cls.os_system_admin.dns_v2.PoolClient()
else:
cls.pool_client = cls.os_admin.dns_v2.PoolClient()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
+ cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
+ cls.share_zone_client = cls.os_primary.dns_v2.SharedZonesClient()
@decorators.idempotent_id('9d2e20fc-e56f-4a62-9c61-9752a9ec615c')
def test_create_zones(self):
# Create a PRIMARY zone
LOG.info('Create a PRIMARY zone')
- zone = self.client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_zones_primary", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Ensure we respond with CREATE+PENDING')
- self.assertEqual('CREATE', zone['action'])
- self.assertEqual('PENDING', zone['status'])
+ self.assertEqual(const.CREATE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
# Get the Name Servers (hosts) created in PRIMARY zone
- nameservers = self.client.show_zone_nameservers(zone['id'])[1]
+ nameservers = self.zones_client.show_zone_nameservers(zone['id'])[1]
nameservers = [dic['hostname'] for dic in nameservers['nameservers']]
# Create a SECONDARY zone
LOG.info('Create a SECONDARY zone')
- zone = self.client.create_zone(
- zone_type=const.SECONDARY_ZONE_TYPE, primaries=nameservers)[1]
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_zones_secondary", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name, zone_type=const.SECONDARY_ZONE_TYPE,
+ primaries=nameservers)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Ensure we respond with CREATE+PENDING')
- self.assertEqual('CREATE', zone['action'])
- self.assertEqual('PENDING', zone['status'])
+ self.assertEqual(const.CREATE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
+
+ # Test with no extra header overrides (sudo-project-id)
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+
+ self.check_CUD_RBAC_enforcement('ZonesClient', 'create_zone',
+ expected_allowed, False)
+
+ # Test with x-auth-sudo-project-id header
+ expected_allowed = ['os_admin']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZonesClient', 'create_zone', expected_allowed, False,
+ project_id=self.zones_client.project_id)
@decorators.idempotent_id('ec150c22-f52e-11eb-b09b-74e5f9e2a801')
def test_create_zone_validate_recordsets_created(self):
# Create a PRIMARY zone and wait till it's Active
LOG.info('Create a PRIMARY zone')
- zone = self.client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_zone_validate_recordsets", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Ensure we respond with CREATE+PENDING')
- self.assertEqual('CREATE', zone['action'])
- self.assertEqual('PENDING', zone['status'])
-
- LOG.info('Wait till the zone is Active')
- waiters.wait_for_zone_status(self.client, zone['id'], 'ACTIVE')
+ self.assertEqual(const.CREATE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
LOG.info('Ensure that SOA and NS recordsets types has been created.')
recordsets = self.recordset_client.list_recordset(
@@ -107,105 +150,336 @@
@decorators.idempotent_id('02ca5d6a-86ce-4f02-9d94-9e5db55c3055')
def test_show_zone(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_zones", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Fetch the zone')
- _, body = self.client.show_zone(zone['id'])
+ body = self.zones_client.show_zone(zone['id'])[1]
LOG.info('Ensure the fetched response matches the created zone')
self.assertExpected(zone, body, self.excluded_keys)
- @decorators.idempotent_id('49268b24-92de-11eb-9d02-74e5f9e2a801')
- def test_show_not_existing_zone(self):
- LOG.info('Fetch non existing zone')
- self.assertRaises(lib_exc.NotFound,
- lambda: self.client.show_zone(uuid.uuid1()))
+ # Test with no extra header overrides (all_projects, sudo-project-id)
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_member',
+ 'os_project_reader'])
- @decorators.idempotent_id('736e3b50-92e0-11eb-9d02-74e5f9e2a801')
- def test_use_invalid_id_to_show_zone(self):
- LOG.info('Fetch the zone using invalid zone ID')
- with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_uuid', 400):
- self.client.show_zone(uuid='zahlabut')
+ self.check_list_show_RBAC_enforcement(
+ 'ZonesClient', 'show_zone', expected_allowed, True, zone['id'])
+
+ # Test with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZonesClient', 'show_zone', expected_allowed, False, zone['id'],
+ headers=self.all_projects_header)
+ self.check_list_show_RBAC_enforcement(
+ 'ZonesClient', 'show_zone', expected_allowed, False, zone['id'],
+ headers={'x-auth-sudo-project-id': self.zones_client.project_id})
+
+ @decorators.idempotent_id('81bff0fb-a5d1-4c64-84db-56ca751c17fc')
+ def test_show_shared_zone(self):
+ if not versionutils.is_compatible('2.1', self.api_version,
+ same_major=False):
+ raise self.skipException(
+ 'Zone share tests require Designate API version 2.1 or newer. '
+ 'Skipping test_show_shared_zone test.')
+
+ LOG.info('Create a zone')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_shared_zone", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ LOG.info('Share the zone with alt')
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+
+ LOG.info('Fetch the zone as alt')
+ body = self.alt_zone_client.show_zone(zone['id'])[1]
+
+ # Account for the zone now being shared
+ zone['shared'] = True
+
+ LOG.info('Ensure the fetched response matches the created zone')
+ self.assertExpected(zone, body, self.excluded_keys)
@decorators.attr(type='smoke')
@decorators.idempotent_id('a4791906-6cd6-4d27-9f15-32273db8bb3d')
def test_delete_zone(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'],
+ zone_name = dns_data_utils.rand_zone_name(
+ name="delete_zones", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
ignore_errors=lib_exc.NotFound)
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement('ZonesClient', 'delete_zone',
+ expected_allowed, True, zone['id'])
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement('ZonesClient', 'delete_zone',
+ expected_allowed, False, zone['id'],
+ headers=self.all_projects_header)
+ self.check_CUD_RBAC_enforcement(
+ 'ZonesClient', 'delete_zone', expected_allowed, False, zone['id'],
+ headers={'x-auth-sudo-project-id': self.zones_client.project_id})
+
LOG.info('Delete the zone')
- _, body = self.client.delete_zone(zone['id'])
+ body = self.zones_client.delete_zone(zone['id'])[1]
LOG.info('Ensure we respond with DELETE+PENDING')
- self.assertEqual('DELETE', body['action'])
- self.assertEqual('PENDING', body['status'])
+ self.assertEqual(const.DELETE, body['action'])
+ self.assertEqual(const.PENDING, body['status'])
- @decorators.idempotent_id('79921370-92e1-11eb-9d02-74e5f9e2a801')
- def test_delete_non_existing_zone(self):
- LOG.info('Delete non existing zone')
+ @decorators.idempotent_id('bf2ee5c1-67b5-47dc-9902-ddb5b0e03e37')
+ def test_delete_zone_with_shares(self):
+
+ if not versionutils.is_compatible('2.1', self.api_version,
+ same_major=False):
+ raise self.skipException(
+ 'Zone share tests require Designate API version 2.1 or newer. '
+ 'Skipping test_delete_zone_with_shares test.')
+
+ LOG.info('Create a zone')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="delete_zones_with_shares", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ LOG.info('Attempt to delete the zone with shares')
+ self.assertRaises(lib_exc.BadRequest, self.zones_client.delete_zone,
+ zone['id'])
+
+ LOG.info('Make sure the zone share is still present')
+ check_share = self.share_zone_client.show_zone_share(
+ zone['id'], shared_zone['id'])[1]
+ self.assertEqual(shared_zone['id'], check_share['id'])
+
+ LOG.info('Delete the zone using delete-shares')
+ body = self.zones_client.delete_zone(
+ zone['id'], delete_shares=True)[1]
+
+ LOG.info('Ensure we respond with DELETE+PENDING')
+ self.assertEqual(const.DELETE, body['action'])
+ self.assertEqual(const.PENDING, body['status'])
+
self.assertRaises(lib_exc.NotFound,
- lambda: self.client.delete_zone(uuid.uuid1()))
+ self.share_zone_client.show_zone_share,
+ zone['id'], shared_zone['id'])
@decorators.idempotent_id('5bfa3cfe-5bc8-443b-bf48-cfba44cbb247')
def test_list_zones(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_zones", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('List zones')
- _, body = self.client.list_zones()
+ body = self.zones_client.list_zones()[1]
- # TODO(kiall): We really want to assert that out newly created zone is
+ # TODO(kiall): We really want to assert that our newly created zone is
# present in the response.
self.assertGreater(len(body['zones']), 0)
+ # Test RBAC - Users that are allowed to call list, but should get
+ # zero zones.
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin', 'os_admin']
+ else:
+ expected_allowed = ['os_alt']
+
+ self.check_list_RBAC_enforcement_count(
+ 'ZonesClient', 'list_zones', expected_allowed, 0)
+
+ # Test that users who should see the zone, can see it.
+ expected_allowed = ['os_primary']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZonesClient', 'list_zones', expected_allowed, [zone['id']])
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZonesClient', 'list_zones', expected_allowed, [zone['id']],
+ headers=self.all_projects_header)
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZonesClient', 'list_zones', expected_allowed, [zone['id']],
+ headers={'x-auth-sudo-project-id': self.zones_client.project_id})
+
+ @decorators.idempotent_id('ad2eed2f-6335-4bc0-87b2-7df7fc4cd82d')
+ def test_list_shared_zone(self):
+ if not versionutils.is_compatible('2.1', self.api_version,
+ same_major=False):
+ raise self.skipException(
+ 'Zone share tests require Designate API version 2.1 or newer. '
+ 'Skipping test_list_shared_zone test.')
+
+ LOG.info('Create zone 1')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_shared_zone_1", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ LOG.info('Create zone 2')
+ zone_2_name = dns_data_utils.rand_zone_name(
+ name="list_shared_zone_2", suffix=self.tld_name)
+ zone_2 = self.zones_client.create_zone(name=zone_2_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone_2['id'])
+
+ LOG.info('Share zone 2 with alt')
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone_2['id'], self.alt_zone_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone_2['id'], shared_zone['id'])
+
+ LOG.info('List zones')
+ body = self.zones_client.list_zones()[1]
+
+ # Check that primary can see all of the zones
+ zone_ids = [item['id'] for item in body['zones']]
+ self.assertIn(zone['id'], zone_ids)
+ self.assertIn(zone_2['id'], zone_ids)
+
+ LOG.info('List zones as alt')
+ body = self.alt_zone_client.list_zones()[1]
+
+ # Make sure alt can only see the zone that was shared with alt
+ zone_ids = [item['id'] for item in body['zones']]
+ self.assertNotIn(zone['id'], zone_ids)
+ self.assertIn(zone_2['id'], zone_ids)
+
@decorators.idempotent_id('123f51cb-19d5-48a9-aacc-476742c02141')
def test_update_zone(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="update_zone", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
# Generate a random description
description = data_utils.rand_name()
LOG.info('Update the zone')
- _, zone = self.client.update_zone(
- zone['id'], description=description)
+ zone = self.zones_client.update_zone(
+ zone['id'], description=description)[1]
LOG.info('Ensure we respond with UPDATE+PENDING')
- self.assertEqual('UPDATE', zone['action'])
- self.assertEqual('PENDING', zone['status'])
+ self.assertEqual(const.UPDATE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
LOG.info('Ensure we respond with updated values')
self.assertEqual(description, zone['description'])
- @decorators.idempotent_id('e391e30a-92e0-11eb-9d02-74e5f9e2a801')
- def test_update_non_existing_zone(self):
- LOG.info('Update non existing zone')
- self.assertRaises(lib_exc.NotFound,
- lambda: self.client.update_zone(
- uuid.uuid1(), description=data_utils.rand_name()))
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
- @decorators.idempotent_id('925192f2-0ed8-4591-8fe7-a9fa028f90a0')
- def test_list_zones_dot_json_fails(self):
- uri = self.client.get_uri('zones.json')
+ self.check_CUD_RBAC_enforcement(
+ 'ZonesClient', 'update_zone', expected_allowed, True,
+ zone['id'], description=description)
- self.assertRaises(lib_exc.NotFound,
- lambda: self.client.get(uri))
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZonesClient', 'update_zone', expected_allowed, False,
+ zone['id'], description=description,
+ headers=self.all_projects_header)
+ self.check_CUD_RBAC_enforcement(
+ 'ZonesClient', 'update_zone', expected_allowed, False,
+ zone['id'], description=description,
+ headers={'x-auth-sudo-project-id': self.zones_client.project_id})
+
+ @decorators.idempotent_id('3acddc86-62cc-4bfa-8589-b99e5d239bf2')
+ @decorators.skip_because(bug="1960487")
+ def test_serial_changes_on_update(self):
+ LOG.info('Create a zone')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="serial_changes_on_update", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ LOG.info("Update Zone's email")
+ update_email = self.zones_client.update_zone(
+ zone['id'], email=dns_data_utils.rand_email())[1]
+ self.assertNotEqual(
+ zone['serial'], update_email['serial'],
+ "Failed, expected: 'Serial' is supposed to be changed "
+ "on Email update.")
+
+ LOG.info("Update Zone's TTL")
+ update_ttl = self.zones_client.update_zone(
+ zone['id'], ttl=dns_data_utils.rand_ttl())[1]
+ self.assertNotEqual(
+ update_email['serial'], update_ttl['serial'],
+ "Failed, expected: 'Serial' is supposed to be changed "
+ "on TTL update.")
+
+ LOG.info("Update Zone's email and description")
+ update_email_description = self.zones_client.update_zone(
+ zone['id'],
+ email=dns_data_utils.rand_email(),
+ description=data_utils.rand_name())[1]
+ self.assertNotEqual(
+ update_ttl['serial'], update_email_description['serial'],
+ "Failed, expect the Serial to change "
+ "when the Email and Description are updated")
+
+ LOG.info("Update Zone's description")
+ update_description = self.zones_client.update_zone(
+ zone['id'], description=data_utils.rand_name())[1]
+ self.assertEqual(
+ update_email_description['serial'], update_description['serial'],
+ "Failed, expect the Serial to not change "
+ "when the Description is updated")
@decorators.idempotent_id('d4ce813e-64a5-11eb-9f43-74e5f9e2a801')
def test_get_primary_zone_nameservers(self):
# Create a zone and get the associated "pool_id"
LOG.info('Create a zone')
- zone = self.client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="get_primary_nameservers", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
zone_pool_id = zone['pool_id']
# Get zone's Name Servers using dedicated API request
- zone_nameservers = self.client.show_zone_nameservers(zone['id'])[1]
+ zone_nameservers = self.zones_client.show_zone_nameservers(
+ zone['id'])[1]
zone_nameservers = zone_nameservers['nameservers']
LOG.info('Zone Name Servers are: {}'.format(zone_nameservers))
self.assertIsNot(
@@ -222,6 +496,49 @@
pool_nameservers, zone_nameservers,
'Failed - Pool and Zone nameservers should be the same')
+ # Test RBAC
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_member',
+ 'os_project_reader'])
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZonesClient', 'show_zone_nameservers', expected_allowed,
+ True, zone['id'])
+
+ # Test with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZonesClient', 'show_zone_nameservers', expected_allowed,
+ False, zone['id'], headers=self.all_projects_header)
+ self.check_list_show_RBAC_enforcement(
+ 'ZonesClient', 'show_zone_nameservers', expected_allowed,
+ False, zone['id'],
+ headers={'x-auth-sudo-project-id': self.zones_client.project_id})
+
+ @decorators.idempotent_id('9970b632-f2db-11ec-a757-201e8823901f')
+ def test_create_zone_ttl_zero(self):
+ LOG.info('Create a PRIMARY zone')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_create_zone_ttl_zero", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name, ttl=0)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ LOG.info('Ensure we respond with CREATE+PENDING')
+ self.assertEqual(const.CREATE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
+
+ LOG.info('Fetch the zone, ensure TTL is Zero')
+ body = self.zones_client.show_zone(zone['id'])[1]
+ self.assertEqual(
+ 0, body['ttl'],
+ "Failed, actual Zone's TTL:{} "
+ "is not Zero".format(body['ttl']))
+
class ZonesAdminTest(BaseZonesTest):
credentials = ["primary", "admin", "system_admin", "alt"]
@@ -239,14 +556,15 @@
cls.admin_client = cls.os_system_admin.dns_v2.ZonesClient()
else:
cls.admin_client = cls.os_admin.dns_v2.ZonesClient()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_client = cls.os_alt.dns_v2.ZonesClient()
@decorators.idempotent_id('f6fe8cce-8b04-11eb-a861-74e5f9e2a801')
def test_show_zone_impersonate_another_project(self):
LOG.info('Create zone "A" using primary client')
- zone = self.client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_zone_impersonate", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('As Alt tenant show zone created by Primary tenant. '
'Expected: 404 NotFound')
@@ -262,49 +580,48 @@
'"x-auth-sudo-project-id" HTTP header. '
'Expected: 403 Forbidden')
self.assertRaises(
- lib_exc.Forbidden, self.alt_client.show_zone, uuid=None,
+ lib_exc.Forbidden, self.alt_client.show_zone, uuid=zone['id'],
headers={'x-auth-sudo-project-id': zone['project_id']})
LOG.info('As Admin user impersonate another project '
'(using "x-auth-sudo-project-id" HTTP header) to show '
'a Primary tenant zone.')
body = self.admin_client.show_zone(
- uuid=None, headers={
+ uuid=zone['id'], headers={
'x-auth-sudo-project-id': zone['project_id']})[1]
LOG.info('Ensure the fetched response matches the impersonated'
' project, it means the ID of a zone "A"')
- self.assertExpected(zone, body['zones'][0], self.excluded_keys)
+ self.assertExpected(zone, body, self.excluded_keys)
@decorators.idempotent_id('e1cf7104-8b06-11eb-a861-74e5f9e2a801')
def test_list_all_projects_zones(self):
LOG.info('Create zone "A" using Primary client')
- primary_zone = self.client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_zone_all_projects_A", suffix=self.tld_name)
+ primary_zone = self.zones_client.create_zone(name=zone_name,
+ wait_until=const.ACTIVE)[1]
self.addCleanup(
- self.wait_zone_delete, self.client, primary_zone['id'])
- LOG.info('Wait till the zone is ACTIVE')
- waiters.wait_for_zone_status(
- self.client, primary_zone['id'], 'ACTIVE')
+ self.wait_zone_delete, self.zones_client, primary_zone['id'])
LOG.info('Create zone "B" using Alt client')
- alt_zone = self.alt_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_zone_all_projects_B", suffix=self.tld_name)
+ alt_zone = self.alt_client.create_zone(name=zone_name,
+ wait_until=const.ACTIVE)[1]
self.addCleanup(
self.wait_zone_delete, self.alt_client, alt_zone['id'])
- LOG.info('Wait till the zone is ACTIVE')
- waiters.wait_for_zone_status(
- self.alt_client, alt_zone['id'], 'ACTIVE')
LOG.info('Create zone "C" using Admin client')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_zone_all_projects_C", suffix=self.tld_name)
admin_zone = self.admin_client.create_zone(
- project_id="FakeProjectID")[1]
+ name=zone_name, project_id="FakeProjectID",
+ wait_until=const.ACTIVE)[1]
self.addCleanup(
self.wait_zone_delete, self.admin_client, admin_zone['id'],
headers=self.all_projects_header)
- LOG.info('Wait till the zone is ACTIVE')
- waiters.wait_for_zone_status(
- self.admin_client, admin_zone['id'], 'ACTIVE',
- headers=self.all_projects_header)
LOG.info('As admin user list all projects zones')
# Note: This is an all-projects list call, so other tests running
@@ -326,7 +643,7 @@
class ZoneOwnershipTest(BaseZonesTest):
- credentials = ["primary", "alt"]
+ credentials = ["primary", "alt", "admin", "system_admin"]
@classmethod
def setup_credentials(cls):
@@ -337,18 +654,19 @@
@classmethod
def setup_clients(cls):
super(ZoneOwnershipTest, cls).setup_clients()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_client = cls.os_alt.dns_v2.ZonesClient()
@decorators.idempotent_id('5d28580a-a012-4b57-b211-e077b1a01340')
def test_no_create_duplicate_domain(self):
LOG.info('Create a zone as a default user')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="no_create_duplicate", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone as an default with existing domain')
self.assertRaises(lib_exc.Conflict,
- self.client.create_zone, name=zone['name'])
+ self.zones_client.create_zone, name=zone['name'])
LOG.info('Create a zone as an alt user with existing domain')
self.assertRaises(lib_exc.Conflict,
@@ -357,8 +675,10 @@
@decorators.idempotent_id('a48776fd-b1aa-4a25-9f09-d1d34cfbb175')
def test_no_create_subdomain_by_alt_user(self):
LOG.info('Create a zone as a default user')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="no_create_subdomain_by_alt", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone as an alt user with existing subdomain')
self.assertRaises(lib_exc.Forbidden,
@@ -368,11 +688,12 @@
@decorators.idempotent_id('f1723d48-c082-43cd-94bf-ebeb5b8c9458')
def test_no_create_superdomain_by_alt_user(self):
- zone_name = dns_data_utils.rand_zone_name()
+ zone_name = dns_data_utils.rand_zone_name(
+ name="no_create_superdomain_by_alt", suffix=self.tld_name)
LOG.info('Create a zone as a default user')
- _, zone = self.client.create_zone(name='a.b.' + zone_name)
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone = self.zones_client.create_zone(name='a.b.' + zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone as an alt user with existing superdomain')
self.assertRaises(lib_exc.Forbidden,
@@ -380,17 +701,14 @@
class ZonesNegativeTest(BaseZonesTest):
+ credentials = ["admin", "primary", "system_admin"]
+
@classmethod
def setup_credentials(cls):
# Do not create network resources for these test.
cls.set_network_resources()
super(ZonesNegativeTest, cls).setup_credentials()
- @classmethod
- def setup_clients(cls):
- super(ZonesNegativeTest, cls).setup_clients()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
-
@decorators.idempotent_id('551853c0-8593-11eb-8c8a-74e5f9e2a801')
def test_no_valid_zone_name(self):
no_valid_names = ['a' * 1000, '___', '!^%&^#%^!@#', 'ggg', '.a', '']
@@ -398,7 +716,7 @@
LOG.info('Trying to create a zone named: {} '.format(name))
self.assertRaisesDns(
lib_exc.BadRequest, 'invalid_object', 400,
- self.client.create_zone, name=name)
+ self.zones_client.create_zone, name=name)
@decorators.idempotent_id('551853c0-8593-11eb-8c8a-74e5f9e2a801')
def test_no_valid_email(self):
@@ -410,25 +728,65 @@
' value: '.format(email))
self.assertRaisesDns(
lib_exc.BadRequest, 'invalid_object', 400,
- self.client.create_zone, email=email)
+ self.zones_client.create_zone, email=email)
@decorators.idempotent_id('551853c0-8593-11eb-8c8a-74e5f9e2a801')
def test_no_valid_ttl(self):
- no_valid_tls = ['zahlabut', -60000,
+ no_valid_ttl = ['zahlabut', -60000,
2147483647 + 10] # Max valid TTL is 2147483647
- for ttl in no_valid_tls:
+ for ttl in no_valid_ttl:
LOG.info(
'Trying to create a zone using: {} as TTL'
' value: '.format(ttl))
self.assertRaisesDns(
lib_exc.BadRequest, 'invalid_object', 400,
- self.client.create_zone, ttl=ttl)
+ self.zones_client.create_zone, ttl=ttl)
+
+ @decorators.idempotent_id('c4d4b92a-86e3-11ee-965f-201e8823901f')
+ def test_not_existing_project_id(self):
+ LOG.info('Trying to create a zone using not existing project_id')
+ self.assertRaises(
+ lib_exc.Forbidden, self.zones_client.create_zone,
+ project_id=data_utils.rand_uuid())
@decorators.idempotent_id('a3b0a928-a682-11eb-9899-74e5f9e2a801')
def test_huge_size_description(self):
LOG.info('Trying to create a zone using huge size description')
self.assertRaisesDns(
lib_exc.BadRequest, 'invalid_object', 400,
- self.client.create_zone,
- description=dns_data_utils.rand_zone_name() * 10000)
+ self.zones_client.create_zone,
+ description=dns_data_utils.rand_zone_name() * 100)
+
+ @decorators.idempotent_id('49268b24-92de-11eb-9d02-74e5f9e2a801')
+ def test_show_not_existing_zone(self):
+ LOG.info('Fetch non existing zone')
+ self.assertRaises(lib_exc.NotFound,
+ lambda: self.zones_client.show_zone(data_utils.rand_uuid()))
+
+ @decorators.idempotent_id('736e3b50-92e0-11eb-9d02-74e5f9e2a801')
+ def test_use_invalid_id_to_show_zone(self):
+ LOG.info('Fetch the zone using invalid zone ID')
+ with self.assertRaisesDns(lib_exc.BadRequest, 'invalid_uuid', 400):
+ self.zones_client.show_zone(uuid='zahlabut')
+
+ @decorators.idempotent_id('79921370-92e1-11eb-9d02-74e5f9e2a801')
+ def test_delete_non_existing_zone(self):
+ LOG.info('Delete non existing zone')
+ self.assertRaises(lib_exc.NotFound,
+ lambda: self.zones_client.delete_zone(data_utils.rand_uuid()))
+
+ @decorators.idempotent_id('e391e30a-92e0-11eb-9d02-74e5f9e2a801')
+ def test_update_non_existing_zone(self):
+ LOG.info('Update non existing zone')
+ self.assertRaises(lib_exc.NotFound,
+ lambda: self.zones_client.update_zone(
+ data_utils.rand_uuid(),
+ description=data_utils.rand_name()))
+
+ @decorators.idempotent_id('925192f2-0ed8-4591-8fe7-a9fa028f90a0')
+ def test_list_zones_dot_json_fails(self):
+ uri = self.zones_client.get_uri('zones.json')
+
+ self.assertRaises(lib_exc.NotFound,
+ lambda: self.zones_client.get(uri))
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 4841d18..5ca5495 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones_exports.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones_exports.py
@@ -16,8 +16,12 @@
from tempest import config
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
+from tempest.lib.common.utils import data_utils
from designate_tempest_plugin.tests import base
+from designate_tempest_plugin.common import waiters
+from designate_tempest_plugin.common import constants as const
+from designate_tempest_plugin import data_utils as dns_data_utils
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -27,9 +31,33 @@
excluded_keys = ['created_at', 'updated_at', 'version', 'links',
'status', 'location']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseZoneExportsTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseZoneExportsTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseZoneExportsTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseZoneExportsTest, cls).resource_cleanup()
+
class ZonesExportTest(BaseZoneExportsTest):
- credentials = ["primary", "admin", "system_admin", "alt"]
+ credentials = ["primary", "admin", "system_admin", "system_reader", "alt",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -44,46 +72,79 @@
cls.admin_client = cls.os_system_admin.dns_v2.ZoneExportsClient()
else:
cls.admin_client = cls.os_admin.dns_v2.ZoneExportsClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
cls.client = cls.os_primary.dns_v2.ZoneExportsClient()
cls.alt_client = cls.os_alt.dns_v2.ZoneExportsClient()
- @decorators.idempotent_id('2dd8a9a0-98a2-4bf6-bb51-286583b30f40')
- def test_create_zone_export(self):
+ def _create_zone_export(self, test_name):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name=test_name, suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone export')
- _, zone_export = self.client.create_zone_export(zone['id'])
+ zone_export = self.client.create_zone_export(zone['id'])[1]
self.addCleanup(self.client.delete_zone_export, zone_export['id'])
+ waiters.wait_for_zone_export_status(
+ self.client, zone_export['id'], const.COMPLETE)
+ return zone, zone_export
+
+ @decorators.idempotent_id('2dd8a9a0-98a2-4bf6-bb51-286583b30f40')
+ def test_create_zone_export(self):
+ zone, zone_export = self._create_zone_export('create_zone_export')
LOG.info('Ensure we respond with PENDING')
- self.assertEqual('PENDING', zone_export['status'])
+ self.assertEqual(const.PENDING, zone_export['status'])
+
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneExportsClient', 'create_zone_export', expected_allowed, True,
+ zone['id'])
@decorators.attr(type='smoke')
@decorators.idempotent_id('2d29a2a9-1941-4b7e-9d8a-ad6c2140ea68')
def test_show_zone_export(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
- LOG.info('Create a zone export')
- resp, zone_export = self.client.create_zone_export(zone['id'])
- self.addCleanup(self.client.delete_zone_export, zone_export['id'])
+ zone_export = self._create_zone_export('show_zone_export')[1]
LOG.info('Re-Fetch the zone export')
- _, body = self.client.show_zone_export(zone_export['id'])
+ body = self.client.show_zone_export(zone_export['id'])[1]
LOG.info('Ensure the fetched response matches the zone export')
self.assertExpected(zone_export, body, self.excluded_keys)
+ # Test RBAC
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_member',
+ 'os_project_reader'])
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZoneExportsClient', 'show_zone_export', expected_allowed, True,
+ zone_export['id'])
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZoneExportsClient', 'show_zone_export', expected_allowed, True,
+ zone_export['id'], headers=self.all_projects_header)
+
@decorators.idempotent_id('fb04507c-9600-11eb-b1cd-74e5f9e2a801')
def test_show_zone_export_impersonate_another_project(self):
LOG.info('Create a zone')
- zone = self.zone_client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name='show_zone_export_impersonate', suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone export using primary client')
resp, zone_export = self.client.create_zone_export(zone['id'])
@@ -102,48 +163,112 @@
'for a primary client: {}'.format(
zone_export['id'], listed_export_ids))
+ # Test RBAC with x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZoneExportsClient', 'show_zone_export', expected_allowed, True,
+ zone_export['id'],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('97234f00-8bcb-43f8-84dd-874f8bc4a80e')
def test_delete_zone_export(self):
LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'],
+ zone_name = dns_data_utils.rand_zone_name(
+ name='delete_zone_export', suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
ignore_errors=lib_exc.NotFound)
LOG.info('Create a zone export')
_, zone_export = self.client.create_zone_export(zone['id'])
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneExportsClient', 'delete_zone_export', expected_allowed, True,
+ zone_export['id'])
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneExportsClient', 'delete_zone_export', expected_allowed, False,
+ zone_export['id'], headers=self.all_projects_header)
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneExportsClient', 'delete_zone_export', expected_allowed, False,
+ zone_export['id'],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
LOG.info('Delete the zone export')
_, body = self.client.delete_zone_export(zone_export['id'])
LOG.info('Ensure the zone export has been successfully deleted')
- self.assertRaises(lib_exc.NotFound,
- lambda: self.client.show_zone_export(zone_export['id']))
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.show_zone_export, zone_export['id'])
@decorators.idempotent_id('476bfdfe-58c8-46e2-b376-8403c0fff440')
def test_list_zone_exports(self):
- LOG.info('Create a zone')
- _, zone = self.zone_client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'])
-
- _, export = self.client.create_zone_export(zone['id'])
- self.addCleanup(self.client.delete_zone_export, export['id'])
+ export = self._create_zone_export('list_zone_exports')[1]
LOG.info('List zone exports')
- _, body = self.client.list_zone_exports()
+ body = self.client.list_zone_exports()[1]
self.assertGreater(len(body['exports']), 0)
+ # Test RBAC - Users that are allowed to call list, but should get
+ # zero zones.
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin', 'os_admin']
+ else:
+ expected_allowed = ['os_alt']
+
+ self.check_list_RBAC_enforcement_count(
+ 'ZoneExportsClient', 'list_zone_exports', expected_allowed, 0)
+
+ # Test that users who should see the zone, can see it.
+ expected_allowed = ['os_primary']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZoneExportsClient', 'list_zone_exports',
+ expected_allowed, [export['id']])
+
+ # Test RBAC with x-auth-sudo-project-id header
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZoneExportsClient', 'list_zone_exports',
+ expected_allowed, [export['id']],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('f34e7f34-9613-11eb-b1cd-74e5f9e2a801')
def test_list_zone_exports_all_projects(self):
LOG.info('Create a primary zone and its export')
- primary_zone = self.zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name='list_zone_exports_all_projects', suffix=self.tld_name)
+ primary_zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(
- self.wait_zone_delete, self.zone_client, primary_zone['id'])
+ self.wait_zone_delete, self.zones_client, primary_zone['id'])
primary_export = self.client.create_zone_export(primary_zone['id'])[1]
self.addCleanup(self.client.delete_zone_export, primary_export['id'])
LOG.info('Create an alt zone and its export')
- alt_zone = self.alt_zone_client.create_zone()[1]
+ alt_zone_name = dns_data_utils.rand_zone_name(
+ name='list_zone_exports_all_projects_alt', suffix=self.tld_name)
+ alt_zone = self.alt_zone_client.create_zone(name=alt_zone_name)[1]
self.addCleanup(
self.wait_zone_delete, self.alt_zone_client, alt_zone['id'])
alt_export = self.alt_client.create_zone_export(alt_zone['id'])[1]
@@ -166,25 +291,41 @@
'Failed, expected ID:{} was not found in '
'listed IDs:{}'.format(id, listed_exports_ids))
+ # Test RBAC with x-auth-all-projects
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZoneExportsClient', 'list_zone_exports', expected_allowed,
+ [alt_export['id']], headers=self.all_projects_header)
+
@decorators.idempotent_id('e4a11a14-9aaa-11eb-be59-74e5f9e2a801')
def test_list_zone_exports_filter_results(self):
LOG.info('Create a primary zone and its export')
- primary_zone = self.zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name='list_zone_exports_filter', suffix=self.tld_name)
+ primary_zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(
- self.wait_zone_delete, self.zone_client, primary_zone['id'])
+ self.wait_zone_delete, self.zones_client, primary_zone['id'])
primary_export = self.client.create_zone_export(primary_zone['id'])[1]
self.addCleanup(self.client.delete_zone_export, primary_export['id'])
LOG.info('Create an alt zone, its export and delete it')
- alt_zone = self.alt_zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name='list_zone_exports_filter_alt', suffix=self.tld_name)
+ alt_zone = self.alt_zone_client.create_zone(name=zone_name)[1]
self.addCleanup(
self.wait_zone_delete, self.alt_zone_client, alt_zone['id'])
alt_export = self.alt_client.create_zone_export(alt_zone['id'])[1]
self.alt_client.delete_zone_export(alt_export['id'])
LOG.info('Ensure the zone export has been successfully deleted')
- self.assertRaises(lib_exc.NotFound,
- lambda: self.alt_client.show_zone_export(alt_export['id']))
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.alt_client.show_zone_export,
+ alt_export['id'])
LOG.info('Filter out "export zones" in status:ZAHLABUT,'
' expected: empty list')
@@ -221,7 +362,7 @@
class ZonesExportTestNegative(BaseZoneExportsTest):
- credentials = ["primary", "alt"]
+ credentials = ["primary", "alt", "admin", "system_admin"]
@classmethod
def setup_credentials(cls):
@@ -232,10 +373,23 @@
@classmethod
def setup_clients(cls):
super(ZonesExportTestNegative, cls).setup_clients()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.client = cls.os_primary.dns_v2.ZoneExportsClient()
cls.alt_client = cls.os_alt.dns_v2.ZoneExportsClient()
+ def _create_zone_export(self, test_name):
+ LOG.info('Create a zone')
+ zone_name = dns_data_utils.rand_zone_name(name=test_name,
+ suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ LOG.info('Create a zone export')
+ zone_export = self.client.create_zone_export(zone['id'])[1]
+ self.addCleanup(self.client.delete_zone_export, zone_export['id'])
+ waiters.wait_for_zone_export_status(
+ self.client, zone_export['id'], const.COMPLETE)
+ return zone, zone_export
+
@decorators.idempotent_id('76ab8ec4-95fd-11eb-b1cd-74e5f9e2a801')
def test_create_zone_export_using_invalid_zone_id(self):
self.assertRaises(
@@ -245,9 +399,11 @@
@decorators.idempotent_id('943dad4a-9617-11eb-b1cd-74e5f9e2a801')
def test_export_not_your_zone(self):
LOG.info('Create a primary zone.')
- primary_zone = self.zone_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(name='export_not_your_zone',
+ suffix=self.tld_name)
+ primary_zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(
- self.wait_zone_delete, self.zone_client, primary_zone['id'])
+ self.wait_zone_delete, self.zones_client, primary_zone['id'])
LOG.info('Make sure that "404 NotFound" status code is raised.')
self.assertRaises(
@@ -257,14 +413,34 @@
@decorators.idempotent_id('518dc308-9604-11eb-b1cd-74e5f9e2a801')
def test_create_zone_export_using_deleted_zone(self):
LOG.info('Create a zone')
- zone = self.zone_client.create_zone()[1]
- self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'],
+ zone_name = dns_data_utils.rand_zone_name(name='export_deleted_zone',
+ suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
ignore_errors=lib_exc.NotFound)
-
LOG.info("Delete the zone and wait till it's done.")
- self.zone_client.delete_zone(zone['id'])[1]
- self.wait_zone_delete(self.zone_client, zone['id'])
+ self.zones_client.delete_zone(zone['id'])[1]
+ self.wait_zone_delete(self.zones_client, zone['id'])
LOG.info('Ensure we respond with NotFound exception')
self.assertRaises(
lib_exc.NotFound, self.client.create_zone_export, zone['id'])
+
+ @decorators.idempotent_id('9a878646-f66b-4fa4-ae95-f3ac3f8e3d31')
+ def test_show_zonefile_using_not_existing_zone_export_id(self):
+ LOG.info('Expected: 404 Not Found zone export')
+ self.assertRaises(lib_exc.NotFound,
+ self.client.show_exported_zonefile,
+ data_utils.rand_uuid())
+
+ @decorators.idempotent_id('52a1fee0-c338-4ed9-b9f9-41ee7fd73375')
+ def test_show_zonefile_not_supported_accept_value(self):
+ zone, zone_export = self._create_zone_export(
+ 'show_zonefile_bad_accept')
+ # Tempest-lib _error_checker will raise UnexpectedResponseCode
+ e = self.assertRaises(
+ lib_exc.UnexpectedResponseCode, self.client.show_exported_zonefile,
+ zone_export['id'], headers={'Accept': 'image/jpeg'})
+ self.assertEqual(406, e.resp.status,
+ "Failed, actual response code is:{0}"
+ "but expected is: 406".format(e.resp.status))
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 510708d..025fa9a 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones_imports.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones_imports.py
@@ -30,9 +30,33 @@
excluded_keys = ['created_at', 'updated_at', 'version', 'links',
'status', 'message', 'zone_id']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseZonesImportTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseZonesImportTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseZonesImportTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseZonesImportTest, cls).resource_cleanup()
+
class ZonesImportTest(BaseZonesImportTest):
- credentials = ["primary", "admin", "system_admin", "alt"]
+ credentials = ["primary", "admin", "system_admin", "system_reader", "alt",
+ "project_member", "project_reader"]
@classmethod
def setup_credentials(cls):
@@ -47,7 +71,6 @@
cls.admin_client = cls.os_system_admin.dns_v2.ZoneImportsClient()
else:
cls.admin_client = cls.os_admin.dns_v2.ZoneImportsClient()
- cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
cls.client = cls.os_primary.dns_v2.ZoneImportsClient()
cls.alt_client = cls.os_alt.dns_v2.ZoneImportsClient()
@@ -57,24 +80,41 @@
waiters.wait_for_zone_import_status(
self.client, zone_import_id, const.COMPLETE)
self.client.delete_zone_import(zone_import['id'])
- self.wait_zone_delete(self.zone_client, zone_import['zone_id'])
+ self.wait_zone_delete(self.zones_client, zone_import['zone_id'])
else: # Import has failed and zone wasn't created.
self.client.delete_zone_import(zone_import['id'])
@decorators.idempotent_id('2e2d907d-0609-405b-9c96-3cb2b87e3dce')
def test_create_zone_import(self):
LOG.info('Create a zone import')
- _, zone_import = self.client.create_zone_import()
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_zone_import", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.client.create_zone_import(
+ zonefile_data=zone_data)[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)
+ # Test with no extra header overrides (sudo-project-id)
+ expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.append('os_system_admin')
+ expected_allowed.append('os_project_member')
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneImportsClient', 'create_zone_import', expected_allowed, False)
+
@decorators.idempotent_id('31eaf25a-9532-11eb-a55d-74e5f9e2a801')
def test_create_zone_import_invalid_ttl(self):
LOG.info('Try to create a zone import using invalid TTL value')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_zone_import_invalid_ttl", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name,
+ ttl='zahlabut')
zone_import = self.client.create_zone_import(
- zonefile_data=dns_data_utils.rand_zonefile_data(ttl='zahlabut'))[1]
+ zonefile_data=zone_data)[1]
self.addCleanup(self.clean_up_resources, zone_import['id'])
waiters.wait_for_zone_import_status(
self.client, zone_import['id'], "ERROR")
@@ -92,7 +132,11 @@
@decorators.idempotent_id('c8909558-0dc6-478a-9e91-eb97b52e59e0')
def test_show_zone_import(self):
LOG.info('Create a zone import')
- _, zone_import = self.client.create_zone_import()
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_zone_import", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.client.create_zone_import(
+ zonefile_data=zone_data)[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(
@@ -104,17 +148,63 @@
LOG.info('Ensure the fetched response matches the expected one')
self.assertExpected(zone_import, body, self.excluded_keys)
+ # Test with no extra header overrides (all_projects, sudo-project-id)
+ expected_allowed = ['os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_project_member',
+ 'os_project_reader'])
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZoneImportsClient', 'show_zone_import', expected_allowed, True,
+ zone_import['id'])
+
+ # Test with x-auth-all-projects
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZoneImportsClient', 'show_zone_import', expected_allowed, False,
+ zone_import['id'], headers=self.all_projects_header)
+
@decorators.idempotent_id('56a16e68-b241-4e41-bc5c-c40747fa68e3')
def test_delete_zone_import(self):
LOG.info('Create a zone import')
- _, zone_import = self.client.create_zone_import()
+ zone_name = dns_data_utils.rand_zone_name(
+ name="delete_zone_import", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.client.create_zone_import(
+ zonefile_data=zone_data)[1]
waiters.wait_for_zone_import_status(self.client, zone_import['id'],
const.COMPLETE)
- _, zone_import = self.client.show_zone_import(zone_import['id'])
+ zone_import = self.client.show_zone_import(zone_import['id'])[1]
self.addCleanup(self.wait_zone_delete,
- self.zone_client,
+ self.zones_client,
zone_import['zone_id'])
+ # Test RBAC
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneImportsClient', 'delete_zone_import', expected_allowed, True,
+ zone_import['id'])
+
+ # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+ expected_allowed = ['os_admin', 'os_primary']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed.extend(['os_system_admin', 'os_project_member'])
+
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneImportsClient', 'delete_zone_import', expected_allowed, False,
+ zone_import['id'], headers=self.all_projects_header)
+ self.check_CUD_RBAC_enforcement(
+ 'ZoneImportsClient', 'delete_zone_import', expected_allowed, False,
+ zone_import['id'],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
LOG.info('Delete the zone')
resp, body = self.client.delete_zone_import(zone_import['id'])
@@ -125,22 +215,58 @@
@decorators.idempotent_id('9eab76af-1995-485f-a2ef-8290c1863aba')
def test_list_zones_imports(self):
LOG.info('Create a zone import')
- _, zone_import = self.client.create_zone_import()
+ zone_name = dns_data_utils.rand_zone_name(
+ name="list_zone_imports", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.client.create_zone_import(
+ zonefile_data=zone_data)[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('List zones imports')
- _, body = self.client.list_zone_imports()
+ body = self.client.list_zone_imports()[1]
self.assertGreater(len(body['imports']), 0)
+ # Test RBAC - Users that are allowed to call list, but should get
+ # zero zones.
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin', 'os_admin']
+ else:
+ expected_allowed = ['os_alt']
+
+ self.check_list_RBAC_enforcement_count(
+ 'ZoneImportsClient', 'list_zone_imports', expected_allowed, 0)
+
+ # Test that users who should see the zone, can see it.
+ expected_allowed = ['os_primary']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZoneImportsClient', 'list_zone_imports', expected_allowed,
+ [zone_import['id']])
+
+ # Test RBAC with x-auth-sudo-project-id header
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZoneImportsClient', 'list_zone_imports', expected_allowed,
+ [zone_import['id']],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('2c1fa20e-9554-11eb-a55d-74e5f9e2a801')
def test_show_import_impersonate_another_project(self):
LOG.info('Import zone "A" using primary client')
- zone_import = self.client.create_zone_import()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="show_zone_import_impersonate", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.client.create_zone_import(
+ zonefile_data=zone_data)[1]
self.addCleanup(self.clean_up_resources, zone_import['id'])
# Make sure we complete the import and have the zone_id for cleanup
@@ -179,10 +305,25 @@
self.assertExpected(
zone_import, resp_body['imports'][0], self.excluded_keys)
+ # Test with x-auth-sudo-project-id header
+ if CONF.enforce_scope.designate:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin', 'os_system_admin']
+
+ self.check_list_show_RBAC_enforcement(
+ 'ZoneImportsClient', 'show_zone_import', expected_allowed, False,
+ zone_import['id'],
+ headers={'x-auth-sudo-project-id': self.client.project_id})
+
@decorators.idempotent_id('7bd06ec6-9556-11eb-a55d-74e5f9e2a801')
def test_list_import_zones_all_projects(self):
LOG.info('Create import zone "A" using primary client')
- zone_import = self.client.create_zone_import()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="_zone_imports_all_projects", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.client.create_zone_import(
+ zonefile_data=zone_data)[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(
@@ -218,3 +359,92 @@
"Failed, expected import ID:{} wasn't found in "
"listed import IDs".format(
zone_import['id'], listed_zone_import_ids))
+
+ # Test RBAC with x-auth-all-projects
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ expected_allowed = ['os_system_admin']
+ else:
+ expected_allowed = ['os_admin']
+
+ self.check_list_IDs_RBAC_enforcement(
+ 'ZoneImportsClient', 'list_zone_imports', expected_allowed,
+ [zone_import['id']], headers=self.all_projects_header)
+
+
+class ZonesImportTestNegative(BaseZonesImportTest):
+ credentials = ["primary", "admin", "system_admin"]
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(ZonesImportTestNegative, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(ZonesImportTestNegative, cls).setup_clients()
+ cls.client = cls.os_primary.dns_v2.ZoneImportsClient()
+
+ def _clean_up_resources(self, zone_import_id):
+ zone_import = self.client.show_zone_import(zone_import_id)[1]
+ if zone_import['zone_id']: # A zone was actually created.
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import_id, const.COMPLETE)
+ self.client.delete_zone_import(zone_import['id'])
+ self.wait_zone_delete(self.zones_client, zone_import['zone_id'])
+ else: # Import has failed and zone wasn't created.
+ self.client.delete_zone_import(zone_import['id'])
+
+ @decorators.idempotent_id('31eaf25a-9532-11eb-a55d-74e5f9e2a801')
+ def test_create_zone_import_invalid_ttl(self):
+ LOG.info('Try to create a zone import using invalid TTL value')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_zone_import_invalid_ttl", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name,
+ ttl='zahlabut')
+ zone_import = self.client.create_zone_import(
+ zonefile_data=zone_data, wait_until=const.ERROR)[1]
+ self.addCleanup(self._clean_up_resources, zone_import['id'])
+
+ @decorators.idempotent_id('31eaf25a-9532-11eb-a55d-74e5f9e2a801')
+ def test_create_zone_import_invalid_name(self):
+ LOG.info('Try to create a zone import using invalid name')
+ zone_import = self.client.create_zone_import(
+ zonefile_data=dns_data_utils.rand_zonefile_data(
+ name='@@@'), wait_until=const.ERROR)[1]
+ self.addCleanup(self._clean_up_resources, zone_import['id'])
+
+ @decorators.idempotent_id('8fd744d2-9dff-11ec-9fb6-201e8823901f')
+ def test_create_zone_import_invalid_file_data(self):
+ LOG.info('Try to create a zone import using random generated'
+ ' import file data')
+ zone_file_data = dns_data_utils.rand_string(size=100)
+ zone_import = self.client.create_zone_import(zone_file_data)[1]
+ self.addCleanup(self.client.delete_zone_import, zone_import['id'])
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import['id'], const.ERROR)
+
+ @decorators.idempotent_id('4fb9494e-9e23-11ec-8378-201e8823901f')
+ def test_zone_cannot_be_update_by_import(self):
+ LOG.info('Create a Zone named: "...zone_to_update..."')
+ zone_name = dns_data_utils.rand_zone_name(
+ name='zone_to_update', suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name, wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ LOG.info('Use zone import to update an existing zone, expected: zone'
+ ' import gets into the ERROR status ')
+ zone_import_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.client.create_zone_import(zone_import_data)[1]
+ waiters.wait_for_zone_import_status(
+ self.client, zone_import['id'], const.ERROR)
+ self.addCleanup(self._clean_up_resources, zone_import['id'])
+
+ @decorators.idempotent_id('5fa8016e-6ed1-11ec-9bd7-201e8823901f')
+ def test_create_zone_import_invalid_content_type(self):
+ LOG.info('Try to create a zone import using: "Content-Type:Zahlabut"'
+ ' HTTP header in POST request')
+ with self.assertRaisesDns(
+ lib_exc.InvalidContentType, 'unsupported_content_type', 415):
+ self.client.create_zone_import(
+ headers={'Content-Type': 'Zahlabut'})
diff --git a/designate_tempest_plugin/tests/api/v2/unauthed_data.json b/designate_tempest_plugin/tests/api/v2/unauthed_data.json
deleted file mode 100644
index c4faf80..0000000
--- a/designate_tempest_plugin/tests/api/v2/unauthed_data.json
+++ /dev/null
@@ -1,127 +0,0 @@
-{
- "list_zones": {
- "client": "zones_client",
- "method": "list_zones"
- },
- "show_zone": {
- "client": "zones_client",
- "method": "show_zone",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "create_zone": {
- "client": "zones_client",
- "method": "create_zone",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "update_zone": {
- "client": "zones_client",
- "method": "update_zone",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "delete_zone": {
- "client": "zones_client",
- "method": "delete_zone",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
-
- "list_recordsets": {
- "client": "recordset_client",
- "method": "list_recordset",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "show_recordset": {
- "client": "recordset_client",
- "method": "show_recordset",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90", "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "create_recordset": {
- "client": "recordset_client",
- "method": "create_recordset",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90", "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "update_recordset": {
- "client": "recordset_client",
- "method": "update_recordset",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90", "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90", {}]
- },
- "delete_recordset": {
- "client": "recordset_client",
- "method": "delete_recordset",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90", "6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
-
- "list_tlds": {
- "client": "tld_client",
- "method": "list_tlds"
- },
- "show_tld": {
- "client": "tld_client",
- "method": "show_tld",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "create_tld": {
- "client": "tld_client",
- "method": "create_tld",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "update_tld": {
- "client": "tld_client",
- "method": "update_tld",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "delete_tld": {
- "client": "tld_client",
- "method": "delete_tld",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
-
- "list_blacklists": {
- "client": "blacklists_client",
- "method": "list_blacklists"
- },
- "show_blacklist": {
- "client": "blacklists_client",
- "method": "show_blacklist",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "create_blacklist": {
- "client": "blacklists_client",
- "method": "create_blacklist",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "update_blacklist": {
- "client": "blacklists_client",
- "method": "update_blacklist",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "delete_blacklist": {
- "client": "blacklists_client",
- "method": "delete_blacklist",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
-
- "list_pools": {
- "client": "pool_client",
- "method": "list_pools"
- },
- "show_pool": {
- "client": "pool_client",
- "method": "show_pool",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "create_pool": {
- "client": "pool_client",
- "method": "create_pool",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "update_pool": {
- "client": "pool_client",
- "method": "update_pool",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- },
- "delete_pool": {
- "client": "pool_client",
- "method": "delete_pool",
- "args": ["6ef3b7f2-df39-43ef-9f37-ce2bc424ab90"]
- }
-}
diff --git a/designate_tempest_plugin/tests/api/v2/valid_txt_dataset.json b/designate_tempest_plugin/tests/api/v2/valid_txt_dataset.json
deleted file mode 100644
index 3abcf10..0000000
--- a/designate_tempest_plugin/tests/api/v2/valid_txt_dataset.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "slash_with_one_trailing_space": {"data": "\"\\ \""},
- "slash_with_many_trailing_space": {"data": "\"\\ \""},
- "text_with_slash_and_trailing_space": {"data": "\"the txts \""}
-}
diff --git a/designate_tempest_plugin/tests/base.py b/designate_tempest_plugin/tests/base.py
index 2e02a8c..26f1f03 100644
--- a/designate_tempest_plugin/tests/base.py
+++ b/designate_tempest_plugin/tests/base.py
@@ -11,13 +11,13 @@
# 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 six
from tempest import test
from tempest import config
from tempest.lib.common.utils import test_utils as utils
-from designate_tempest_plugin.services.dns.query.query_client import \
- QueryClient
+from designate_tempest_plugin.services.dns.query.query_client import (
+ QueryClient)
+from designate_tempest_plugin.tests import rbac_utils
CONF = config.CONF
@@ -55,7 +55,7 @@
return False
-class BaseDnsTest(test.BaseTestCase):
+class BaseDnsTest(rbac_utils.RBACTestsMixin, test.BaseTestCase):
"""Base class for DNS tests."""
# NOTE(andreaf) credentials holds a list of the credentials to be allocated
@@ -64,9 +64,23 @@
# rest the actual roles.
# NOTE(kiall) primary will result in a manager @ cls.os_primary, alt will
# have cls.os_alt, and admin will have cls.os_admin.
- # NOTE(kiall) We should default to only primary, and request additional
- # credentials in the tests that require them.
- credentials = ['primary']
+ # NOTE(johnsom) We will allocate most credentials here so that each test
+ # can test for allowed and disallowed RBAC policies.
+ credentials = ['admin', 'primary', 'alt']
+ if CONF.dns_feature_enabled.enforce_new_defaults:
+ credentials.extend(['system_admin', 'system_reader',
+ 'project_member', 'project_reader'])
+
+ # A tuple of credentials that will be allocated by tempest using the
+ # 'credentials' list above. These are used to build RBAC test lists.
+ allocated_creds = []
+ for cred in credentials:
+ if isinstance(cred, list):
+ allocated_creds.append('os_roles_' + cred[0])
+ else:
+ allocated_creds.append('os_' + cred)
+ # Tests shall not mess with the list of allocated credentials
+ allocated_credentials = tuple(allocated_creds)
@classmethod
def skip_checks(cls):
@@ -89,9 +103,20 @@
build_interval=CONF.dns.build_interval,
build_timeout=CONF.dns.build_timeout,
)
+ # Most tests need a "primary" zones client and we need it for the
+ # API version check, so create one instance here.
+ cls.zones_client = cls.os_primary.dns_v2.ZonesClient()
+
+ @classmethod
+ def resource_setup(cls):
+ """Setup resources needed by the tests."""
+ super(BaseDnsTest, cls).resource_setup()
+
+ # The credential does not matter here.
+ cls.api_version = cls.zones_client.get_max_api_version()
def assertExpected(self, expected, actual, excluded_keys):
- for key, value in six.iteritems(expected):
+ for key, value in expected.items():
if key not in excluded_keys:
self.assertIn(key, actual)
self.assertEqual(value, actual[key], key)
@@ -135,6 +160,10 @@
zone_id,
recordset_id)
+ def unset_ptr(self, ptr_client, fip_id, **kwargs):
+ return utils.call_and_ignore_notfound_exc(
+ ptr_client.unset_ptr_record, fip_id, **kwargs)
+
def _delete_zone(self, zone_client, zone_id, **kwargs):
return utils.call_and_ignore_notfound_exc(zone_client.delete_zone,
zone_id, **kwargs)
@@ -159,6 +188,7 @@
"""Base class for DNS V2 API tests."""
all_projects_header = {'X-Auth-All-Projects': True}
+ managed_records = {'x-designate-edit-managed-records': True}
@classmethod
def skip_checks(cls):
diff --git a/designate_tempest_plugin/tests/rbac_utils.py b/designate_tempest_plugin/tests/rbac_utils.py
new file mode 100644
index 0000000..aa8bb6a
--- /dev/null
+++ b/designate_tempest_plugin/tests/rbac_utils.py
@@ -0,0 +1,368 @@
+# Copyright 2021 Red Hat, Inc. All rights reserved.
+#
+# 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 copy
+
+from oslo_log import log as logging
+from tempest import config
+from tempest.lib import exceptions
+from tempest import test
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class RBACTestsMixin(test.BaseTestCase):
+
+ def _get_client_method(self, cred_obj, client_str, method_str):
+ """Get requested method from registered clients in Tempest."""
+ dns_clients = getattr(cred_obj, 'dns_v2')
+ client = getattr(dns_clients, client_str)
+ client_obj = client()
+ method = getattr(client_obj, method_str)
+ return method
+
+ def _get_client_project_id(self, cred_obj, client_str):
+ """Get project ID for the credential."""
+ dns_clients = getattr(cred_obj, 'dns_v2')
+ client = getattr(dns_clients, client_str)
+ client_obj = client()
+ return client_obj.project_id
+
+ def _check_allowed(self, client_str, method_str, allowed_list,
+ with_project, *args, **kwargs):
+ """Test an API call allowed RBAC enforcement.
+
+ :param client_str: The service client to use for the test, without the
+ credential. Example: 'ZonesClient'
+ :param method_str: The method on the client to call for the test.
+ Example: 'list_zones'
+ :param allowed_list: The list of credentials expected to be
+ allowed. Example: ['primary'].
+ :param with_project: When true, pass the project ID to the call.
+ :param args: Any positional parameters needed by the method.
+ :param kwargs: Any named parameters needed by the method.
+ :raises AssertionError: Raised if the RBAC tests fail.
+ :raises Forbidden: Raised if a credential that should have access does
+ not and is denied.
+ :raises InvalidScope: Raised if a credential that should have the
+ correct scope for access is denied.
+ :returns: None on success
+ """
+ for cred in allowed_list:
+ try:
+ cred_obj = getattr(self, cred)
+ except AttributeError:
+ # TODO(johnsom) Remove once scoped tokens is the default.
+ if ((cred == 'os_system_admin' or
+ cred == 'os_system_reader') and
+ not CONF.enforce_scope.designate):
+ LOG.info('Skipping %s allowed RBAC test because '
+ 'enforce_scope.designate is not True', cred)
+ continue
+ else:
+ self.fail('Credential {} "expected_allowed" for RBAC '
+ 'testing was not created by tempest '
+ 'credentials setup. This is likely a bug in the '
+ 'test.'.format(cred))
+ method = self._get_client_method(cred_obj, client_str, method_str)
+ project_id = self._get_client_project_id(cred_obj, client_str)
+ try:
+ if with_project:
+ method(project_id, *args, **kwargs)
+ else:
+ method(*args, **kwargs)
+ except exceptions.Forbidden as e:
+ self.fail('Method {}.{} failed to allow access via RBAC using '
+ 'credential {}. Error: {}'.format(
+ client_str, method_str, cred, str(e)))
+ except exceptions.NotFound as e:
+ self.fail('Method {}.{} failed to allow access via RBAC using '
+ 'credential {}. Error: {}'.format(
+ client_str, method_str, cred, str(e)))
+
+ def _check_disallowed(self, client_str, method_str, allowed_list,
+ expect_404, with_project, *args, **kwargs):
+ """Test an API call disallowed RBAC enforcement.
+
+ :param client_str: The service client to use for the test, without the
+ credential. Example: 'ZonesClient'
+ :param method_str: The method on the client to call for the test.
+ Example: 'list_zones'
+ :param allowed_list: The list of credentials expected to be
+ allowed. Example: ['primary'].
+ :param expect_404: When True, 404 responses are considered ok.
+ :param with_project: When true, pass the project ID to the call.
+ :param args: Any positional parameters needed by the method.
+ :param kwargs: Any named parameters needed by the method.
+ :raises AssertionError: Raised if the RBAC tests fail.
+ :raises Forbidden: Raised if a credential that should have access does
+ not and is denied.
+ :raises InvalidScope: Raised if a credential that should have the
+ correct scope for access is denied.
+ :returns: None on success
+ """
+ expected_disallowed = (set(self.allocated_credentials) -
+ set(allowed_list))
+ for cred in expected_disallowed:
+ cred_obj = getattr(self, cred)
+ method = self._get_client_method(cred_obj, client_str, method_str)
+ project_id = self._get_client_project_id(cred_obj, client_str)
+
+ # Unfortunately tempest uses testtools assertRaises[1] which means
+ # we cannot use the unittest assertRaises context[2] with msg= to
+ # give a useful error.
+ # Also, testtools doesn't work with subTest[3], so we can't use
+ # that to expose the failing credential.
+ # This all means the exception raised testtools assertRaises
+ # is less than useful.
+ # TODO(johnsom) Remove this try block once testtools is useful.
+ # [1] https://testtools.readthedocs.io/en/latest/
+ # api.html#testtools.TestCase.assertRaises
+ # [2] https://docs.python.org/3/library/
+ # unittest.html#unittest.TestCase.assertRaises
+ # [3] https://github.com/testing-cabal/testtools/issues/235
+ try:
+ if with_project:
+ method(project_id, *args, **kwargs)
+ else:
+ method(*args, **kwargs)
+ except exceptions.Forbidden:
+ continue
+ except exceptions.NotFound:
+ # Some APIs hide that the resource exists by returning 404
+ # on permission denied.
+ if expect_404:
+ continue
+ raise
+ self.fail('Method {}.{} failed to deny access via RBAC using '
+ 'credential {}.'.format(client_str, method_str, cred))
+
+ def check_list_show_RBAC_enforcement(self, client_str, method_str,
+ expected_allowed, expect_404,
+ *args, **kwargs):
+ """Test list or show API call RBAC enforcement.
+
+ :param client_str: The service client to use for the test, without the
+ credential. Example: 'ZonesClient'
+ :param method_str: The method on the client to call for the test.
+ Example: 'list_zones'
+ :param expected_allowed: The list of credentials expected to be
+ allowed. Example: ['primary'].
+ :param expect_404: When True, 404 responses are considered ok.
+ :param args: Any positional parameters needed by the method.
+ :param kwargs: Any named parameters needed by the method.
+ :raises AssertionError: Raised if the RBAC tests fail.
+ :raises Forbidden: Raised if a credential that should have access does
+ not and is denied.
+ :raises InvalidScope: Raised if a credential that should have the
+ correct scope for access is denied.
+ :returns: None on success
+ """
+
+ allowed_list = copy.deepcopy(expected_allowed)
+
+ # #### Test that disallowed credentials cannot access the API.
+ self._check_disallowed(client_str, method_str, allowed_list,
+ expect_404, False, *args, **kwargs)
+
+ # #### Test that allowed credentials can access the API.
+ self._check_allowed(client_str, method_str, allowed_list, False,
+ *args, **kwargs)
+
+ def check_list_show_with_ID_RBAC_enforcement(self, client_str, method_str,
+ expected_allowed, expect_404,
+ *args, **kwargs):
+ """Test list or show API call passing the project ID RBAC enforcement.
+
+ :param client_str: The service client to use for the test, without the
+ credential. Example: 'ZonesClient'
+ :param method_str: The method on the client to call for the test.
+ Example: 'list_zones'
+ :param expected_allowed: The list of credentials expected to be
+ allowed. Example: ['primary'].
+ :param expect_404: When True, 404 responses are considered ok.
+ :param args: Any positional parameters needed by the method.
+ :param kwargs: Any named parameters needed by the method.
+ :raises AssertionError: Raised if the RBAC tests fail.
+ :raises Forbidden: Raised if a credential that should have access does
+ not and is denied.
+ :raises InvalidScope: Raised if a credential that should have the
+ correct scope for access is denied.
+ :returns: None on success
+ """
+
+ allowed_list = copy.deepcopy(expected_allowed)
+
+ # #### Test that disallowed credentials cannot access the API.
+ self._check_disallowed(client_str, method_str, allowed_list,
+ expect_404, True, *args, **kwargs)
+
+ # #### Test that allowed credentials can access the API.
+ self._check_allowed(client_str, method_str, allowed_list, True,
+ *args, **kwargs)
+
+ def check_CUD_RBAC_enforcement(self, client_str, method_str,
+ expected_allowed, expect_404,
+ *args, **kwargs):
+ """Test an API create/update/delete call RBAC enforcement.
+
+ :param client_str: The service client to use for the test, without the
+ credential. Example: 'ZonesClient'
+ :param method_str: The method on the client to call for the test.
+ Example: 'list_zones'
+ :param expected_allowed: The list of credentials expected to be
+ allowed. Example: ['primary'].
+ :param expect_404: When True, 404 responses are considered ok.
+ :param args: Any positional parameters needed by the method.
+ :param kwargs: Any named parameters needed by the method.
+ :raises AssertionError: Raised if the RBAC tests fail.
+ :raises Forbidden: Raised if a credential that should have access does
+ not and is denied.
+ :raises InvalidScope: Raised if a credential that should have the
+ correct scope for access is denied.
+ :returns: None on success
+ """
+
+ allowed_list = copy.deepcopy(expected_allowed)
+
+ # #### Test that disallowed credentials cannot access the API.
+ self._check_disallowed(client_str, method_str, allowed_list,
+ expect_404, False, *args, **kwargs)
+
+ def check_list_RBAC_enforcement_count(
+ self, client_str, method_str, expected_allowed, expected_count,
+ *args, **kwargs):
+ """Test an API list call RBAC enforcement result count.
+
+ List APIs will return the object list for the project associated
+ with the token used to access the API. This means most credentials
+ will have access, but will get differing results.
+
+ This test will query the list API using a list of credentials and
+ will validate that only the expected count of results are returned.
+
+ :param client_str: The service client to use for the test, without the
+ credential. Example: 'ZonesClient'
+ :param method_str: The method on the client to call for the test.
+ Example: 'list_zones'
+ :param expected_allowed: The list of credentials expected to be
+ allowed. Example: ['primary'].
+ :param expected_count: The number of results expected in the list
+ returned from the API.
+ :param args: Any positional parameters needed by the method.
+ :param kwargs: Any named parameters needed by the method.
+ :raises AssertionError: Raised if the RBAC tests fail.
+ :raises Forbidden: Raised if a credential that should have access does
+ not and is denied.
+ :raises InvalidScope: Raised if a credential that should have the
+ correct scope for access is denied.
+ :returns: None on success
+ """
+
+ allowed_list = copy.deepcopy(expected_allowed)
+
+ for cred in allowed_list:
+ try:
+ cred_obj = getattr(self, cred)
+ except AttributeError:
+ # TODO(johnsom) Remove once scoped tokens is the default.
+ if ((cred == 'os_system_admin' or
+ cred == 'os_system_reader') and
+ not CONF.enforce_scope.designate):
+ LOG.info('Skipping %s allowed RBAC test because '
+ 'enforce_scope.designate is not True', cred)
+ continue
+ else:
+ self.fail('Credential {} "expected_allowed" for RBAC '
+ 'testing was not created by tempest '
+ 'credentials setup. This is likely a bug in the '
+ 'test.'.format(cred))
+ method = self._get_client_method(cred_obj, client_str, method_str)
+ try:
+ # Get the result body
+ result = method(*args, **kwargs)[1]
+ except exceptions.Forbidden as e:
+ self.fail('Method {}.{} failed to allow access via RBAC using '
+ 'credential {}. Error: {}'.format(
+ client_str, method_str, cred, str(e)))
+ # Remove the root element
+ result_objs = next(iter(result.values()))
+
+ self.assertEqual(expected_count, len(result_objs),
+ message='Credential {} saw {} objects when {} '
+ 'was expected.'.format(cred, len(result_objs),
+ expected_count))
+
+ def check_list_IDs_RBAC_enforcement(
+ self, client_str, method_str, expected_allowed, expected_ids,
+ *args, **kwargs):
+ """Test an API list call RBAC enforcement result contains IDs.
+
+ List APIs will return the object list for the project associated
+ with the token used to access the API. This means most credentials
+ will have access, but will get differing results.
+
+ This test will query the list API using a list of credentials and
+ will validate that the expected object Ids in included in the results.
+
+ :param client_str: The service client to use for the test, without the
+ credential. Example: 'ZonesClient'
+ :param method_str: The method on the client to call for the test.
+ Example: 'list_zones'
+ :param expected_allowed: The list of credentials expected to be
+ allowed. Example: ['primary'].
+ :param expected_ids: The list of object IDs to validate are included
+ in the returned list from the API.
+ :param args: Any positional parameters needed by the method.
+ :param kwargs: Any named parameters needed by the method.
+ :raises AssertionError: Raised if the RBAC tests fail.
+ :raises Forbidden: Raised if a credential that should have access does
+ not and is denied.
+ :raises InvalidScope: Raised if a credential that should have the
+ correct scope for access is denied.
+ :returns: None on success
+ """
+
+ allowed_list = copy.deepcopy(expected_allowed)
+
+ for cred in allowed_list:
+ try:
+ cred_obj = getattr(self, cred)
+ except AttributeError:
+ # TODO(johnsom) Remove once scoped tokens is the default.
+ if ((cred == 'os_system_admin' or
+ cred == 'os_system_reader') and
+ not CONF.enforce_scope.designate):
+ LOG.info('Skipping %s allowed RBAC test because '
+ 'enforce_scope.designate is not True', cred)
+ continue
+ else:
+ self.fail('Credential {} "expected_allowed" for RBAC '
+ 'testing was not created by tempest '
+ 'credentials setup. This is likely a bug in the '
+ 'test.'.format(cred))
+ method = self._get_client_method(cred_obj, client_str, method_str)
+ try:
+ # Get the result body
+ result = method(*args, **kwargs)[1]
+ except exceptions.Forbidden as e:
+ self.fail('Method {}.{} failed to allow access via RBAC using '
+ 'credential {}. Error: {}'.format(
+ client_str, method_str, cred, str(e)))
+ # Remove the root element
+ result_objs = next(iter(result.values()))
+
+ result_ids = [result_obj["id"] for result_obj in result_objs]
+ self.assertTrue(set(expected_ids).issubset(set(result_ids)))
diff --git a/designate_tempest_plugin/tests/scenario/v2/recordset_data.json b/designate_tempest_plugin/tests/scenario/v2/recordset_data.json
deleted file mode 100644
index 3168722..0000000
--- a/designate_tempest_plugin/tests/scenario/v2/recordset_data.json
+++ /dev/null
@@ -1,84 +0,0 @@
-{
- "A": {
- "name": "www",
- "type": "A",
- "records": ["192.0.2.1", "192.0.2.2", "192.0.2.3"]
- },
- "AAAA": {
- "name": "www",
- "type": "AAAA",
- "records": ["2001:db8::1", "2001:db8::1", "2001:db8::"]
- },
- "SRV TCP": {
- "name": "_sip._tcp",
- "type": "SRV",
- "records": ["10 60 5060 server1.example.com.",
- "20 60 5060 server2.example.com.",
- "20 30 5060 server3.example.com."]
- },
- "SRV UDP": {
- "name": "_sip._udp",
- "type": "SRV",
- "records": ["10 60 5060 server1.example.com.",
- "10 60 5060 server2.example.com.",
- "20 30 5060 server3.example.com."]
- },
- "CNAME": {
- "name": "alias-of-target",
- "type": "CNAME",
- "records": ["target.example.org."]
- },
- "MX at APEX": {
- "name": null,
- "type": "MX",
- "records": ["10 mail1.example.org.",
- "20 mail2.example.org."]
- },
- "MX under APEX": {
- "name": "under",
- "type": "MX",
- "records": ["10 mail.example.org."]
- },
- "SSHFP": {
- "name": "www",
- "type": "SSHFP",
- "records": ["2 1 123456789abcdef67890123456789abcdef67890"]
- },
- "TXT": {
- "name": "www",
- "type": "TXT",
- "records": ["\"Any Old Text Goes Here\""]
- },
- "SPF": {
- "name": "*.sub",
- "type": "SPF",
- "records": ["\"v=spf1; a -all\""]
- },
- "NS": {
- "name": "NS_Record",
- "type": "NS",
- "records": ["ns1.example.org."]
- },
- "PTR_IPV4": {
- "name": "PTR_Record_IPV4",
- "type": "PTR",
- "records": ["34.216.184.93.in-addr.arpa."]
- },
- "PTR_IPV6":{
- "name":"PTR_Record_IPV6",
- "type":"PTR",
- "records":[
- "6.4.9.1.8.c.5.2.3.9.8.1.8.4.2.0.1.0.0.0.0.2.2.0.0.0.8.2.6.0.6.2.ip6.arpa."
- ]
- },
- "CAA_Record": {
- "name": "CAA_Record",
- "type": "CAA",
- "records": ["0 issue letsencrypt.org"]
- },
- "NAPTR_Record": {
- "name": "NAPTR_Record",
- "type": "NAPTR",
- "records": ["0 0 S SIP+D2U !^.*$!sip:customer-service@example.com! _sip._udp.example.com."]
- }
-}
\ No newline at end of file
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_blacklists.py b/designate_tempest_plugin/tests/scenario/v2/test_blacklists.py
index 27b3f7f..85d5d5a 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_blacklists.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_blacklists.py
@@ -27,6 +27,29 @@
class BaseBlacklistsTest(base.BaseDnsV2Test):
excluded_keys = ['created_at', 'updated_at', 'links']
+ @classmethod
+ def setup_clients(cls):
+ super(BaseBlacklistsTest, cls).setup_clients()
+
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseBlacklistsTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="BaseBlacklistsTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(BaseBlacklistsTest, cls).resource_cleanup()
+
class BlacklistE2E(BaseBlacklistsTest):
@@ -48,7 +71,6 @@
else:
cls.admin_blacklist_client = cls.os_admin.dns_v2.BlacklistsClient()
cls.admin_zone_client = cls.os_admin.dns_v2.ZonesClient()
- cls.primary_zone_client = cls.os_primary.dns_v2.ZonesClient()
@decorators.idempotent_id('22b1ee72-d8d2-11eb-bcdc-74e5f9e2a801')
def test_primary_fails_to_create_zone_matches_blacklist_regex(self):
@@ -64,7 +86,7 @@
'"blacklistregextest".')
self.assertRaisesDns(
lib_exc.BadRequest, 'invalid_zone_name', 400,
- self.primary_zone_client.create_zone,
+ self.zones_client.create_zone,
name='blacklistregextest' + dns_data_utils.rand_zone_name())
@decorators.idempotent_id('6956f20c-d8d5-11eb-bcdc-74e5f9e2a801')
@@ -81,31 +103,49 @@
LOG.info('Try to create a zone named:{}'.format(zone_name))
self.assertRaisesDns(
lib_exc.BadRequest, 'invalid_zone_name', 400,
- self.primary_zone_client.create_zone, name=zone_name)
+ self.zones_client.create_zone, name=zone_name)
@decorators.idempotent_id('de030088-d97e-11eb-8ab8-74e5f9e2a801')
def test_admin_creates_zone_matches_blacklist_name_or_regex(self):
- LOG.info('Create a blacklists using: regex and exact string(name)')
- zone_name = 'blacklistnameregextest1' + dns_data_utils.rand_zone_name()
+ LOG.info('Create two blacklists: by regex and by exact string')
+ zone_name_to_deny = dns_data_utils.rand_zone_name(
+ name="deny_by_name", suffix=self.tld_name)
blacklists = [
{'pattern': '^blacklistnameregextest2.*',
- 'description': 'Zone starts with "a" char'},
- {'pattern': zone_name,
- 'description': 'Deny if Zone named:{} '.format(zone_name)}]
+ 'description': 'Zone starts with "blacklistnameregextest2"'},
+ {'pattern': zone_name_to_deny,
+ 'description': 'Deny if Zone named:{} '.format(
+ zone_name_to_deny)}]
for blacklist in blacklists:
body = self.admin_blacklist_client.create_blacklist(**blacklist)[1]
self.addCleanup(
self.admin_blacklist_client.delete_blacklist, body['id'])
- LOG.info('As Admin user try to create zones that are '
- 'supposed to be blocked')
+ LOG.info('Primary tries to create a zone that is blacklisted by name.'
+ ' Expected: FAIL')
+ with self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_zone_name', 400):
+ self.zones_client.create_zone(name=zone_name_to_deny)
+
+ LOG.info('Admin tries to create a zone that is blacklisted by name '
+ 'for a Primary user. Expected: FAIL')
zone = self.admin_zone_client.create_zone(
- name='blacklistnameregextest2' +
- dns_data_utils.rand_zone_name(),
- project_id=self.primary_zone_client.project_id)[1]
+ name=zone_name_to_deny,
+ project_id=self.zones_client.project_id)[1]
self.addCleanup(
- self.wait_zone_delete, self.admin_zone_client, zone['id'])
+ self.wait_zone_delete, self.zones_client, zone['id'])
+
+ LOG.info('Primary tries to create a zone that is blacklisted by regex.'
+ ' Expected: FAIL')
+ with self.assertRaisesDns(
+ lib_exc.BadRequest, 'invalid_zone_name', 400):
+ self.zones_client.create_zone(
+ name='blacklistnameregextest2{}'.format(zone_name_to_deny))
+
+ LOG.info('Admin tries to create a zone that is blacklisted by regex'
+ ' for a Primary user. Expected: FAIL')
zone = self.admin_zone_client.create_zone(
- name=zone_name, project_id=self.primary_zone_client.project_id)[1]
+ name='blacklistnameregextest2{}'.format(zone_name_to_deny),
+ project_id=self.zones_client.project_id)[1]
self.addCleanup(
- self.wait_zone_delete, self.admin_zone_client, zone['id'])
+ self.wait_zone_delete, self.zones_client, zone['id'])
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_classless_ptr.py b/designate_tempest_plugin/tests/scenario/v2/test_classless_ptr.py
new file mode 100644
index 0000000..c149893
--- /dev/null
+++ b/designate_tempest_plugin/tests/scenario/v2/test_classless_ptr.py
@@ -0,0 +1,227 @@
+# Copyright 2022 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.
+import time
+
+from oslo_utils import versionutils
+from tempest import config
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from designate_tempest_plugin.tests import base
+from designate_tempest_plugin.services.dns.query.query_client import (
+ SingleQueryClient)
+
+CONF = config.CONF
+
+
+# This test suite is intended to test RFC 2317 classless in-addr.arpa
+# delegation scenarios.
+class ClasslessPTRTest(base.BaseDnsV2Test):
+
+ credentials = ['primary', 'admin', 'system_admin', 'alt']
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these tests.
+ cls.set_network_resources()
+ super(ClasslessPTRTest, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(ClasslessPTRTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.zone_client = cls.os_primary.dns_v2.ZonesClient()
+ cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
+ cls.alt_rec_client = cls.os_alt.dns_v2.RecordsetClient()
+ cls.share_zone_client = cls.os_primary.dns_v2.SharedZonesClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(ClasslessPTRTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ cls.tld_name = '0.192.in-addr-arpa'
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=cls.tld_name)
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(ClasslessPTRTest, cls).resource_cleanup()
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('f2d9596c-ce87-4dfd-9bb4-ad2430fd3fe6')
+ def test_classless_ptr_delegation(self):
+ # Create full subnet zone
+ zone_name = f'2.{self.tld_name}.'
+ zone = self.zone_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ # Create the delegated zone
+ delegated_zone_name = f'1-3.2.{self.tld_name}.'
+ delegated_zone = self.zone_client.create_zone(
+ name=delegated_zone_name, wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zone_client,
+ delegated_zone['id'], ignore_errors=lib_exc.NotFound)
+
+ # Create the PTR record in the delegated zone
+ ptr_recordset_data = {
+ 'name': f'1.1-3.2.{self.tld_name}.',
+ 'type': 'PTR',
+ 'records': ['www.example.org.']
+ }
+ ptr_recordset = self.recordset_client.create_recordset(
+ delegated_zone['id'], ptr_recordset_data, wait_until='ACTIVE')[1]
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.recordset_client.delete_recordset,
+ delegated_zone['id'], ptr_recordset['id'])
+
+ # Create the CNAME record
+ cname_recordset_data = {
+ 'name': f'1.2.{self.tld_name}.',
+ 'type': 'CNAME',
+ 'records': [f'1.1-3.2.{self.tld_name}.']
+ }
+ cname_recordset = self.recordset_client.create_recordset(
+ zone['id'], cname_recordset_data, wait_until='ACTIVE')[1]
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.recordset_client.delete_recordset,
+ zone['id'], cname_recordset['id'])
+
+ # Check for a CNAME record
+ if config.CONF.dns.nameservers:
+ ns = config.CONF.dns.nameservers[0]
+ start = time.time()
+ while True:
+ ns_obj = SingleQueryClient(ns, config.CONF.dns.query_timeout)
+ ns_record = ns_obj.query(
+ cname_recordset['name'],
+ rdatatype=cname_recordset_data['type'])
+ if cname_recordset_data['records'][0] in str(ns_record):
+ break
+ if time.time() - start >= config.CONF.dns.build_timeout:
+ raise lib_exc.TimeoutException(
+ 'Failed, CNAME record was not detected on '
+ 'Nameserver:{} within a timeout of:{}'
+ ' seconds.'.format(ns, config.CONF.dns.build_timeout))
+
+ # Check for a PTR record
+ if config.CONF.dns.nameservers:
+ ns = config.CONF.dns.nameservers[0]
+ start = time.time()
+ while True:
+ ns_obj = SingleQueryClient(ns, config.CONF.dns.query_timeout)
+ ns_record = ns_obj.query(
+ ptr_recordset['name'],
+ rdatatype=ptr_recordset_data['type'])
+ if ptr_recordset_data['records'][0] in str(ns_record):
+ break
+ if time.time() - start >= config.CONF.dns.build_timeout:
+ raise lib_exc.TimeoutException(
+ 'Failed, PTR record was not detected on '
+ 'Nameserver:{} within a timeout of:{}'
+ ' seconds.'.format(ns, config.CONF.dns.build_timeout))
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('0110e7b1-9582-410e-b3d5-bd38a1265222')
+ def test_classless_ptr_delegation_shared_zone(self):
+
+ if not versionutils.is_compatible('2.1', self.api_version,
+ same_major=False):
+ raise self.skipException(
+ 'Zone share tests require Designate API version 2.1 or newer. '
+ 'Skipping test_classless_ptr_delegation_shared_zone test.')
+
+ # Create full subnet zone
+ zone_name = f'2.{self.tld_name}.'
+ zone = self.zone_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zone_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ # Create the delegated zone
+ delegated_zone_name = f'1-3.2.{self.tld_name}.'
+ delegated_zone = self.zone_client.create_zone(
+ name=delegated_zone_name, wait_until='ACTIVE')[1]
+ self.addCleanup(self.wait_zone_delete, self.zone_client,
+ delegated_zone['id'], ignore_errors=lib_exc.NotFound)
+
+ # Create the CNAME record
+ cname_recordset_data = {
+ 'name': f'1.2.{self.tld_name}.',
+ 'type': 'CNAME',
+ 'records': [f'1.1-3.2.{self.tld_name}.']
+ }
+ cname_recordset = self.recordset_client.create_recordset(
+ zone['id'], cname_recordset_data, wait_until='ACTIVE')[1]
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.recordset_client.delete_recordset,
+ zone['id'], cname_recordset['id'])
+
+ # Share the zone with the alt credential
+ shared_zone = self.share_zone_client.create_zone_share(
+ delegated_zone['id'], self.alt_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ delegated_zone['id'], shared_zone['id'])
+
+ # Create the PTR record in the delegated zone as the alt project
+ ptr_recordset_data = {
+ 'name': f'1.1-3.2.{self.tld_name}.',
+ 'type': 'PTR',
+ 'records': ['www.example.org.']
+ }
+ ptr_recordset = self.alt_rec_client.create_recordset(
+ delegated_zone['id'], ptr_recordset_data, wait_until='ACTIVE')[1]
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.alt_rec_client.delete_recordset,
+ delegated_zone['id'], ptr_recordset['id'])
+
+ # Check for a CNAME record
+ if config.CONF.dns.nameservers:
+ ns = config.CONF.dns.nameservers[0]
+ start = time.time()
+ while True:
+ ns_obj = SingleQueryClient(ns, config.CONF.dns.query_timeout)
+ ns_record = ns_obj.query(
+ cname_recordset['name'],
+ rdatatype=cname_recordset_data['type'])
+ if cname_recordset_data['records'][0] in str(ns_record):
+ break
+ if time.time() - start >= config.CONF.dns.build_timeout:
+ raise lib_exc.TimeoutException(
+ 'Failed, CNAME record was not detected on '
+ 'Nameserver:{} within a timeout of:{}'
+ ' seconds.'.format(ns, config.CONF.dns.build_timeout))
+
+ # Check for a PTR record
+ if config.CONF.dns.nameservers:
+ ns = config.CONF.dns.nameservers[0]
+ start = time.time()
+ while True:
+ ns_obj = SingleQueryClient(ns, config.CONF.dns.query_timeout)
+ ns_record = ns_obj.query(
+ ptr_recordset['name'],
+ rdatatype=ptr_recordset_data['type'])
+ if ptr_recordset_data['records'][0] in str(ns_record):
+ break
+ if time.time() - start >= config.CONF.dns.build_timeout:
+ raise lib_exc.TimeoutException(
+ 'Failed, PTR record was not detected on '
+ 'Nameserver:{} within a timeout of:{}'
+ ' seconds.'.format(ns, config.CONF.dns.build_timeout))
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_quotas.py b/designate_tempest_plugin/tests/scenario/v2/test_quotas.py
index 0c20b3d..7f6bc07 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_quotas.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_quotas.py
@@ -11,13 +11,21 @@
# 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 random
from oslo_log import log as logging
+from oslo_utils import versionutils
from tempest import config
from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.common.utils import data_utils
+
+import tempest.test
from designate_tempest_plugin.tests import base
from designate_tempest_plugin import data_utils as dns_data_utils
+from designate_tempest_plugin.common import constants as const
+from designate_tempest_plugin.common import exceptions
+
LOG = logging.getLogger(__name__)
@@ -28,6 +36,7 @@
class QuotasV2Test(base.BaseDnsV2Test):
credentials = ['primary', 'admin', 'system_admin', 'alt']
+ test_quota_limit = 3
@classmethod
def setup_credentials(cls):
@@ -49,44 +58,504 @@
super(QuotasV2Test, cls).setup_clients()
if CONF.enforce_scope.designate:
cls.admin_client = cls.os_system_admin.dns_v2.QuotasClient()
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
else:
cls.admin_client = cls.os_admin.dns_v2.QuotasClient()
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
cls.quotas_client = cls.os_primary.dns_v2.QuotasClient()
cls.alt_client = cls.os_alt.dns_v2.QuotasClient()
cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
+ cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
- @decorators.idempotent_id('6987953a-dccf-11eb-903e-74e5f9e2a801')
- def test_alt_reaches_zones_quota(self):
+ @classmethod
+ def resource_setup(cls):
+ super(QuotasV2Test, cls).resource_setup()
- alt_project_id = self.alt_client.project_id
- http_header = {'x-auth-sudo-project-id': alt_project_id}
- limit_zones_quota = 3
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="QuotasV2Test")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
- LOG.info('As Admin user set Zones quota for Alt user '
- 'to:{} '.format(limit_zones_quota))
- quotas = dns_data_utils.rand_quotas()
- quotas['zones'] = limit_zones_quota
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(QuotasV2Test, cls).resource_cleanup()
+
+ def _set_quota_for_project(self, project_id, quotas):
+ http_header = {'x-auth-sudo-project-id': project_id}
self.admin_client.set_quotas(
- project_id=alt_project_id, quotas=quotas, headers=http_header)
+ project_id=project_id, quotas=quotas, headers=http_header)
self.addCleanup(
- self.admin_client.delete_quotas, project_id=alt_project_id)
+ self.admin_client.delete_quotas,
+ project_id=project_id, headers=http_header)
- LOG.info('As Alt user try to create zones, up untill'
- ' "zones" quota (status code 413) is reached')
+ def _reach_quota_limit(
+ self, limit_threshold, quota_type, zone=None):
attempt_number = 0
- while attempt_number <= limit_zones_quota + 1:
- attempt_number += 1
- LOG.info('Attempt No:{} '.format(attempt_number))
+ not_raised_msg = ("Failed, expected '413 over_quota' response of "
+ "type:{} wasn't received.".format(quota_type))
+ while attempt_number <= limit_threshold + 1:
try:
- zone = self.alt_zone_client.create_zone()[1]
- self.addCleanup(
- self.wait_zone_delete, self.alt_zone_client, zone['id'])
- except Exception as err:
- raised_error = str(err).replace(' ', '')
- if not "'code':413" and "'type':'over_quota'" in raised_error \
- and attempt_number == limit_zones_quota + 1:
- raise (
- "Failed, expected status code 413 (type:over_quota) "
- "was not raised or maybe it has been raised mistakenly"
- "(bug) before the quota was actually reached."
- " Test failed with: {} ".format(err))
+ attempt_number += 1
+ LOG.info('Attempt No:{} '.format(attempt_number))
+ if quota_type == 'zones_quota':
+ zone_name = dns_data_utils.rand_zone_name(
+ name="_reach_quota_limit", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name,
+ description='Test zone for:{}'.format(quota_type))[1]
+ self.addCleanup(
+ self.wait_zone_delete,
+ self.zones_client, zone['id'])
+ else:
+ if quota_type == 'zone_recordsets':
+ max_number_of_records = 10
+ prj_quota = self.admin_client.show_quotas(
+ project_id=self.zones_client.project_id,
+ headers=self.all_projects_header)[1][
+ 'zone_records']
+ if max_number_of_records > prj_quota:
+ max_number_of_records = prj_quota
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'],
+ number_of_records=random.randint(
+ 1, max_number_of_records))
+ else:
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ recordset = self.recordset_client.create_recordset(
+ zone['id'], recordset_data=recordset_data,
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_recordset_delete,
+ self.recordset_client,
+ zone['id'], recordset['id'])
+ self.assertLess(
+ attempt_number, limit_threshold + 1, not_raised_msg)
+ except Exception as e:
+ raised_err = str(e).replace(' ', '')
+ if not_raised_msg in str(e):
+ raise AssertionError(not_raised_msg)
+ elif ("'code':413" in raised_err and
+ "'type':'over_quota'" in raised_err):
+ LOG.info("OK, type':'over_quota' was raised")
+ break
+ else:
+ raise
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('41d9cf2c-866a-11ec-8ccb-201e8823901f')
+ @decorators.skip_because(bug="1960495")
+ def test_api_export_size_quota(self):
+ LOG.info('Admin sets "api_export_size:{}" quota for Primary'
+ ' user'.format(self.test_quota_limit))
+ quotas = dns_data_utils.rand_quotas()
+ quotas['api_export_size'] = self.test_quota_limit
+ self._set_quota_for_project(
+ self.zones_client.project_id, quotas)
+ LOG.info('Create a Zone, wait until ACTIVE and add:{}'
+ ' Recordsets'.format(self.test_quota_limit + 1))
+ zone = self.zones_client.create_zone(
+ description='Zone for test_api_export_size_quota',
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_zone_delete,
+ self.zones_client, zone['id'])
+ for i in range(self.test_quota_limit + 1):
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ LOG.info('Try to create a recordset No:{}'.format(i))
+ recordset = self.recordset_client.create_recordset(
+ zone['id'], recordset_data=recordset_data,
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_recordset_delete,
+ self.recordset_client,
+ zone['id'], recordset['id'])
+ LOG.info(
+ 'Ensure that the Number of Recordsets is bigger than configured'
+ ' api_export_size:{}'.format(self.test_quota_limit))
+ number_of_recordsets = len(self.recordset_client.list_recordset(
+ zone['id'])[1]['recordsets'])
+ self.assertGreater(
+ number_of_recordsets, self.test_quota_limit,
+ 'Failed, the number of recordsets within a Zone is not enough to'
+ ' trigger "413 over quota" on Zone Export')
+ LOG.info('Try to export Zone. Expected:"413 over_quota"')
+ with self.assertRaisesDns(
+ lib_exc.OverLimit, 'over_quota', 413):
+ self.export_zone_client.create_zone_export(zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('2513cb6e-85ec-11ec-bf7f-201e8823901f')
+ def test_recordset_records_quota(self):
+ LOG.info('Admin sets "recordset_records:{}" quota for Primary'
+ ' user'.format(self.test_quota_limit))
+ quotas = dns_data_utils.rand_quotas()
+ quotas['recordset_records'] = self.test_quota_limit
+ self._set_quota_for_project(
+ self.zones_client.project_id, quotas)
+ LOG.info('Create a Zone and wait until ACTIVE')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_recordset_records_quota", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name,
+ description='Zone for test_recordset_records_quota',
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_zone_delete,
+ self.zones_client, zone['id'])
+ LOG.info(
+ 'Create recordset data with:{} records and try to create'
+ ' a recordset. Expected:"413 over_quota"'.format(
+ self.test_quota_limit + 1))
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'],
+ number_of_records=self.test_quota_limit + 1)
+ LOG.info('Try to create a recordset. Expected:"413 over_quota"')
+ with self.assertRaisesDns(
+ lib_exc.OverLimit, 'over_quota', 413):
+ self.recordset_client.create_recordset(
+ zone['id'], recordset_data=recordset_data)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('893dc648-868d-11ec-8ccb-201e8823901f')
+ def test_zone_records_quota(self):
+ LOG.info('Create a Zone and wait until ACTIVE')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_zone_records_quota", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name,
+ description='Zone for test_zone_records_quota',
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_zone_delete,
+ self.zones_client, zone['id'])
+ LOG.info('Admin sets "zone_records:{}" quota for Primary '
+ 'user'.format(self.test_quota_limit))
+ quotas = dns_data_utils.rand_quotas()
+ quotas['zone_records'] = self.test_quota_limit
+ self._set_quota_for_project(
+ self.zones_client.project_id, quotas)
+ LOG.info(
+ 'Try to add:{} recordsets (with a single record) to the Zone in'
+ ' loop. Expected:"413 over_quota"'.format(
+ self.test_quota_limit + 1))
+ self._reach_quota_limit(
+ self.test_quota_limit + 1, 'zone_records', zone)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('f567bdda-86b3-11ec-8ccb-201e8823901f')
+ def test_zone_recordsets_quota(self):
+ LOG.info('Create a Zone and wait until ACTIVE')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_zone_recordsets_quota", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name,
+ description='Zone for test_zone_recordsets_quota',
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_zone_delete,
+ self.zones_client, zone['id'])
+ LOG.info('Admin sets "zone_recordsets:{}" quota for Primary '
+ 'user'.format(self.test_quota_limit))
+ quotas = dns_data_utils.rand_quotas()
+ quotas['zone_recordsets'] = self.test_quota_limit
+ self._set_quota_for_project(
+ self.zones_client.project_id, quotas)
+ LOG.info(
+ 'Try to add:{} recordsets (with a random number of records) to a'
+ ' Zone in loop. Expected:"413 over_quota"'.format(
+ self.test_quota_limit + 1))
+ self._reach_quota_limit(
+ self.test_quota_limit + 1,
+ 'zone_recordsets', zone)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('6987953a-dccf-11eb-903e-74e5f9e2a801')
+ def test_zones_quota(self):
+ LOG.info('Admin sets "zones" quota for Primary user')
+ quotas = dns_data_utils.rand_quotas()
+ quotas['zones'] = self.test_quota_limit
+ self._set_quota_for_project(
+ self.zones_client.project_id, quotas)
+ LOG.info('Try to create Zones. Expected:"413 over_quota"')
+ self._reach_quota_limit(self.test_quota_limit, 'zones_quota')
+
+
+class QuotasBoundary(base.BaseDnsV2Test, tempest.test.BaseTestCase):
+
+ credentials = ['admin', 'system_admin', 'primary']
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(QuotasBoundary, cls).setup_credentials()
+
+ @classmethod
+ def skip_checks(cls):
+ super(QuotasBoundary, cls).skip_checks()
+ if not CONF.dns_feature_enabled.api_v2_quotas:
+ skip_msg = ("%s skipped as designate V2 Quotas API is not "
+ "available" % cls.__name__)
+ raise cls.skipException(skip_msg)
+
+ @classmethod
+ def setup_clients(cls):
+ super(QuotasBoundary, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ cls.quota_client = cls.os_system_admin.dns_v2.QuotasClient()
+ cls.project_client = cls.os_system_admin.projects_client
+ cls.recordset_client = cls.os_system_admin.dns_v2.RecordsetClient()
+ cls.export_zone_client = (
+ cls.os_system_admin.dns_v2.ZoneExportsClient())
+ cls.admin_zones_client = cls.os_system_admin.dns_v2.ZonesClient()
+ else:
+ cls.quota_client = cls.os_admin.dns_v2.QuotasClient()
+ cls.project_client = cls.os_admin.projects_client
+ cls.admin_zones_client = cls.os_admin.dns_v2.ZonesClient()
+ cls.recordset_client = cls.os_admin.dns_v2.RecordsetClient()
+ cls.export_zone_client = cls.os_admin.dns_v2.ZoneExportsClient()
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(QuotasBoundary, cls).resource_setup()
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="QuotasBoundary")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(QuotasBoundary, cls).resource_cleanup()
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('e4981eb2-3803-11ed-9d3c-201e8823901f')
+ def test_zone_quota_boundary(self):
+ # Create a dedicated Project for Boundary tests
+ tenant_id = self.project_client.create_project(
+ name=data_utils.rand_name(name='BoundaryZone'))['project']['id']
+ self.addCleanup(self.project_client.delete_project, tenant_id)
+
+ # Set Quotas (zones:1) for tested project
+ sudo_header = {'x-auth-sudo-project-id': tenant_id}
+ quotas = {
+ 'zones': 1, 'zone_recordsets': 2, 'zone_records': 3,
+ 'recordset_records': 2, 'api_export_size': 3}
+ self.quota_client.set_quotas(
+ project_id=tenant_id, quotas=quotas,
+ headers=sudo_header)
+
+ # Create a first Zone --> Should PASS
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_zone_quota_boundary_attempt_1", suffix=self.tld_name)
+ zone = self.admin_zones_client.create_zone(
+ name=zone_name, project_id=tenant_id)[1]
+ self.addCleanup(self.wait_zone_delete, self.admin_zones_client,
+ zone['id'])
+
+ # Create a second zone --> should FAIL on: 413 over_quota
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_zone_quota_boundary_attempt_2", suffix=self.tld_name)
+ try:
+ response_headers, zone = self.admin_zones_client.create_zone(
+ name=zone_name, project_id=tenant_id)
+ if response_headers['status'] != 413:
+ raise exceptions.InvalidStatusError(
+ 'Zone', zone['id'], zone['status'])
+ except Exception as e:
+ self.assertIn('over_quota', str(e),
+ 'Failed, over_quota for a zone was not raised')
+ finally:
+ self.addCleanup(
+ self.wait_zone_delete,
+ self.admin_zones_client, zone['id'],
+ headers=sudo_header,
+ ignore_errors=lib_exc.NotFound)
+
+
+class SharedZonesQuotaTest(base.BaseDnsV2Test):
+ credentials = ['primary', 'admin', 'system_admin']
+
+ @classmethod
+ def setup_clients(cls):
+ super(SharedZonesQuotaTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ cls.adm_project_client = cls.os_system_admin.projects_client
+ cls.adm_quota_client = cls.os_system_admin.dns_v2.QuotasClient()
+ cls.adm_zone_client = cls.os_system_admin.dns_v2.ZonesClient()
+ cls.adm_shr_client = cls.os_system_admin.dns_v2.SharedZonesClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.adm_project_client = cls.os_admin.projects_client
+ cls.adm_quota_client = cls.os_admin.dns_v2.QuotasClient()
+ cls.adm_zone_client = cls.os_admin.dns_v2.ZonesClient()
+ cls.adm_shr_client = cls.os_admin.dns_v2.SharedZonesClient()
+ cls.share_zone_client = cls.os_primary.dns_v2.SharedZonesClient()
+ cls.rec_client = cls.os_primary.dns_v2.RecordsetClient()
+ cls.export_zone_client = cls.os_primary.dns_v2.ZoneExportsClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(SharedZonesQuotaTest, cls).resource_setup()
+
+ if not versionutils.is_compatible('2.1', cls.api_version,
+ same_major=False):
+ raise cls.skipException(
+ 'The shared zones scenario tests require Designate API '
+ 'version 2.1 or newer. Skipping Shared Zones scenario tests.')
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name='SharedZonesTest')
+ cls.tld_name = f'.{tld_name}'
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(SharedZonesQuotaTest, cls).resource_cleanup()
+
+ def _create_shared_zone_for_project(
+ self, zone_name, project_id, sudo_header):
+ """Admin creates Zone for project ID and shares it with Primary"""
+ zone_name = dns_data_utils.rand_zone_name(
+ name=zone_name,
+ suffix=self.tld_name)
+ zone = self.adm_zone_client.create_zone(
+ name=zone_name, project_id=project_id, wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_zone_delete, self.adm_zone_client, zone['id'],
+ headers=sudo_header, delete_shares=True)
+ shared_zone = self.adm_shr_client.create_zone_share(
+ zone['id'], self.rec_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(self.adm_shr_client.delete_zone_share,
+ zone['id'], shared_zone['id'], headers=sudo_header)
+ return zone, shared_zone
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('31968688-c0d3-11ed-b04f-201e8823901f')
+ @decorators.skip_because(bug="1992445")
+ def test_zone_recordsets_enforced_against_owner(self):
+
+ # Create a dedicated Project "A" for shared zone test
+ tenant_id = self.adm_project_client.create_project(
+ name=data_utils.rand_name(
+ name='SharedZonesQuotaTest'))['project']['id']
+ self.addCleanup(self.adm_project_client.delete_project, tenant_id)
+
+ # Set Quotas "zone_recordsets:1" for a project "A"
+ sudo_header = {'x-auth-sudo-project-id': tenant_id}
+ quotas = {
+ 'zones': 7, 'zone_recordsets': 1, 'zone_records': 7,
+ 'recordset_records': 7, 'api_export_size': 7}
+ self.adm_quota_client.set_quotas(
+ project_id=tenant_id, quotas=quotas,
+ headers=sudo_header)
+
+ # Admin creates a zone for project "A" and shares it Primary
+ zone = self._create_shared_zone_for_project(
+ zone_name='test_zone_recordsets_enforced_against_owner',
+ project_id=tenant_id, sudo_header=sudo_header)[0]
+
+ # Primary creates a first recodset - should PASS
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ recordset = self.rec_client.create_recordset(
+ zone['id'], recordset_data)[1]
+ self.addCleanup(self.wait_recordset_delete, self.rec_client,
+ zone['id'], recordset['id'], ignore_errors=lib_exc.NotFound)
+
+ # Primary creates a second recodset - should FAIL
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ with self.assertRaisesDns(
+ lib_exc.OverLimit, 'over_quota', 413):
+ self.rec_client.create_recordset(zone['id'], recordset_data)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('cbb8d6b4-c64e-11ed-80d8-201e8823901f')
+ def test_zone_recocrds_enforced_against_owner(self):
+
+ # Create a dedicated Project "A" for shared zone test
+ tenant_id = self.adm_project_client.create_project(
+ name=data_utils.rand_name(
+ name='SharedZonesQuotaTest'))['project']['id']
+ self.addCleanup(self.adm_project_client.delete_project, tenant_id)
+
+ # Set Quotas "zone_records:1" for a project "A"
+ sudo_header = {'x-auth-sudo-project-id': tenant_id}
+ quotas = {
+ 'zones': 7, 'zone_recordsets': 7, 'zone_records': 1,
+ 'recordset_records': 7, 'api_export_size': 7}
+ self.adm_quota_client.set_quotas(
+ project_id=tenant_id, quotas=quotas,
+ headers=sudo_header)
+
+ # Admin creates a zone for project "A" and share it with Primary
+ zone = self._create_shared_zone_for_project(
+ zone_name='test_zone_recocrds_enforced_against_owner',
+ project_id=tenant_id, sudo_header=sudo_header)[0]
+
+ # Primary creates recordset with (single record) --> Should PASS
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ body = self.rec_client.create_recordset(
+ zone['id'], recordset_data)[1]
+ self.addCleanup(
+ self.wait_recordset_delete, self.rec_client,
+ zone['id'], body['id'])
+
+ # Primary creates one more recordset (single record) --> Should FAIL
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ with self.assertRaisesDns(
+ lib_exc.OverLimit, 'over_quota', 413):
+ self.rec_client.create_recordset(
+ zone['id'], recordset_data)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('40b436f0-c895-11ed-8900-201e8823901f')
+ def test_recordset_records_enforced_against_owner(self):
+
+ # Create a dedicated Project "A" for shared zone test
+ tenant_id = self.adm_project_client.create_project(
+ name=data_utils.rand_name(
+ name='SharedZonesQuotaTest'))['project']['id']
+ self.addCleanup(self.adm_project_client.delete_project, tenant_id)
+
+ # Set Quotas "zone_records:1" for a project "A"
+ sudo_header = {'x-auth-sudo-project-id': tenant_id}
+ quotas = {
+ 'zones': 7, 'zone_recordsets': 7, 'zone_records': 7,
+ 'recordset_records': 1, 'api_export_size': 7}
+ self.adm_quota_client.set_quotas(
+ project_id=tenant_id, quotas=quotas,
+ headers=sudo_header)
+
+ # Admin creates a zone for project "A" and share it with Primary
+ zone = self._create_shared_zone_for_project(
+ zone_name='test_recordset_records_enforced_against_owner',
+ project_id=tenant_id, sudo_header=sudo_header)[0]
+
+ # Primary creates recordset with (single record) --> Should PASS
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ body = self.rec_client.create_recordset(
+ zone['id'], recordset_data)[1]
+ self.addCleanup(
+ self.wait_recordset_delete, self.rec_client,
+ zone['id'], body['id'])
+
+ # Primary creates one more recordset (two records) --> Should FAIL
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'], number_of_records=2)
+ with self.assertRaisesDns(
+ lib_exc.OverLimit, 'over_quota', 413):
+ self.rec_client.create_recordset(
+ zone['id'], recordset_data)
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_recordsets.py b/designate_tempest_plugin/tests/scenario/v2/test_recordsets.py
index 4c40c28..07ddde0 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_recordsets.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_recordsets.py
@@ -9,23 +9,28 @@
# 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 time
+
from oslo_log import log as logging
from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
-import ddt
+import testtools
from designate_tempest_plugin.tests import base
+from designate_tempest_plugin.common import constants as const
+from designate_tempest_plugin import data_utils as dns_data_utils
from designate_tempest_plugin.common import waiters
-
+from designate_tempest_plugin.services.dns.query.query_client import (
+ SingleQueryClient)
LOG = logging.getLogger(__name__)
CONF = config.CONF
-@ddt.ddt
class RecordsetsTest(base.BaseDnsV2Test):
credentials = ["admin", "system_admin", "primary"]
@@ -35,9 +40,10 @@
super(RecordsetsTest, cls).setup_clients()
if CONF.enforce_scope.designate:
cls.admin_client = cls.os_system_admin.dns_v2.RecordsetClient()
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
else:
cls.admin_client = cls.os_admin.dns_v2.RecordsetClient()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
@classmethod
@@ -47,24 +53,33 @@
zone_id = CONF.dns.zone_id
if zone_id:
LOG.info('Retrieve info from a zone')
- _, zone = cls.client.show_zone(zone_id)
+ zone = cls.zones_client.show_zone(zone_id)[1]
else:
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="RecordsetsTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(
+ tld_name=tld_name[:-1])
LOG.info('Create a new zone')
- _, zone = cls.client.create_zone()
+ zone_name = dns_data_utils.rand_zone_name(
+ name="recordsets_test_setup", suffix=cls.tld_name)
+ zone = cls.zones_client.create_zone(name=zone_name)[1]
cls.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
- cls.client.delete_zone, zone['id'])
+ cls.zones_client.delete_zone, zone['id'])
LOG.info('Ensure we respond with ACTIVE')
- waiters.wait_for_zone_status(cls.client, zone['id'], 'ACTIVE')
+ waiters.wait_for_zone_status(cls.zones_client, zone['id'], 'ACTIVE')
cls.zone = zone
- @decorators.attr(type='slow')
- @decorators.idempotent_id('4664ed66-9ff1-45f2-9e60-d4913195c505')
- @ddt.file_data("recordset_data.json")
- def test_create_and_delete_records_on_existing_zone(self, name,
- type, records):
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(RecordsetsTest, cls).resource_cleanup()
+
+ def _test_create_and_delete_records_on_existing_zone(
+ self, name, type, records):
if name is not None:
recordset_name = name + "." + self.zone['name']
@@ -78,8 +93,8 @@
}
LOG.info('Create a Recordset on the existing zone')
- _, recordset = self.recordset_client.create_recordset(
- self.zone['id'], recordset_data)
+ recordset = self.recordset_client.create_recordset(
+ self.zone['id'], recordset_data)[1]
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.recordset_client.delete_recordset,
self.zone['id'], recordset['id'])
@@ -87,38 +102,261 @@
LOG.info('Ensure we respond with PENDING')
self.assertEqual('PENDING', recordset['status'])
- LOG.info('Wait until the recordset is active')
+ LOG.info('Wait until the recordset is active and propagated')
waiters.wait_for_recordset_status(self.recordset_client,
self.zone['id'], recordset['id'],
'ACTIVE')
+ waiters.wait_for_query(
+ self.query_client, recordset_data['name'], type)
LOG.info('Delete the recordset')
- _, body = self.recordset_client.delete_recordset(self.zone['id'],
- recordset['id'])
+ body = self.recordset_client.delete_recordset(
+ self.zone['id'], recordset['id'])[1]
LOG.info('Ensure we respond with DELETE+PENDING')
self.assertEqual('DELETE', body['action'])
self.assertEqual('PENDING', body['status'])
- LOG.info('Ensure successful deletion of Recordset')
+ LOG.info('Ensure successful deletion of Recordset from:'
+ ' Designate and Backends')
self.assertRaises(lib_exc.NotFound,
lambda: self.recordset_client.show_recordset(
self.zone['id'], recordset['id']))
+ waiters.wait_for_query(
+ self.query_client, recordset_data['name'], type, found=False)
- @decorators.idempotent_id('1e78a742-66ee-11ec-8dc3-201e8823901f')
- def test_create_soa_record_not_permitted(self):
- # SOA record is automatically created for a zone, no user
- # should be able to create a SOA record.
- soa_record = ("s1.devstack.org. admin.example.net. 1510721487 3510"
- " 600 86400 3600")
- LOG.info('Primary tries to create a Recordset on '
- 'the existing zone')
- self.assertRaises(
- lib_exc.BadRequest,
- self.recordset_client.create_recordset,
- self.zone['id'], soa_record)
- LOG.info('Admin tries to create a Recordset on the existing zone')
- self.assertRaises(
- lib_exc.BadRequest,
- self.admin_client.create_recordset,
- self.zone['id'], soa_record)
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('4664ed66-9ff1-45f2-9e60-d4913195c505')
+ def test_create_and_delete_records_on_existing_zone_01_A(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "www", "A", ["192.0.2.1", "192.0.2.2", "192.0.2.3"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('cecd9f20-0b62-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_02_AAAA(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "www", "AAAA", ["2001:db8::1", "2001:db8::1", "2001:db8::"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('f5368d7a-0b62-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_03_SRV(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "_sip._tcp", "SRV", ["10 60 5060 server1.example.com.",
+ "20 60 5060 server2.example.com.",
+ "20 30 5060 server3.example.com."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('74ff9efc-0b63-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_04_SRV(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "_sip._udp", "SRV", ["10 60 5060 server1.example.com.",
+ "10 60 5060 server2.example.com.",
+ "20 30 5060 server3.example.com."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('82a14a2e-0b63-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_05_CNAME(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "alias-of-target", "CNAME", ["target.example.org."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('ae7a295e-0b63-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_06_MX(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ None, "MX", ["10 mail1.example.org.",
+ "20 mail2.example.org."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('f9aa8512-0b64-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_07_MX(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "under", "MX", ["10 mail.example.org."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('fa6cbd12-0b64-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_08_SSHFP(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "www", "SSHFP", ["2 1 123456789abcdef67890123456789abcdef67890"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('fa124a1c-0b64-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_09_TXT(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "www", "TXT", ["\"Any Old Text Goes Here\""])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('3e347c28-0b66-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_10_SPF(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "*.sub", "SPF", ["\"v=spf1; a -all\""])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('88f6c2ac-0b66-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_11_PTR(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "PTR_Record_IPV4", "PTR", ["34.216.184.93.in-addr.arpa."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('b9591eea-0b66-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_12_PTR(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "PTR_Record_IPV6", "PTR",
+ ["6.4.9.1.8.c.5.2.3.9.8.1.8.4.2.0.1.0.0.0.0.2.2.0.0.0.8"
+ ".2.6.0.6.2.ip6.arpa."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('c98cd9b4-0b66-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_13_CAA(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "CAA_Record", "CAA", ["0 issue letsencrypt.org"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('f78d6e8c-0b66-11ee-bbcc-201e8823901f')
+ def test_create_and_delete_records_on_existing_zone_14_NAPTR(self):
+ self._test_create_and_delete_records_on_existing_zone(
+ "NAPTR_Record", "NAPTR",
+ ["0 0 S SIP+D2U !^.*$!sip:customer-service@example"
+ ".com! _sip._udp.example.com."])
+
+ @testtools.skipUnless(
+ config.CONF.dns.nameservers,
+ "Config option dns.nameservers is missing or empty")
+ def _test_update_records_propagated_to_backends(self, name, type, records):
+ if name:
+ recordset_name = name + "." + self.zone['name']
+ else:
+ recordset_name = self.zone['name']
+
+ orig_ttl = 666
+ updated_ttl = 777
+ recordset_data = {
+ 'name': recordset_name,
+ 'type': type,
+ 'records': records,
+ 'ttl': orig_ttl
+ }
+
+ LOG.info('Create a Recordset on the existing zone')
+ recordset = self.recordset_client.create_recordset(
+ self.zone['id'], recordset_data, wait_until=const.ACTIVE)[1]
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.recordset_client.delete_recordset,
+ self.zone['id'], recordset['id'])
+
+ LOG.info('Update a Recordset on the existing zone')
+ recordset_data['ttl'] = updated_ttl
+ self.recordset_client.update_recordset(
+ self.zone['id'], recordset['id'],
+ recordset_data, wait_until=const.ACTIVE)
+
+ LOG.info('Per Nameserver "dig" for a record until either:'
+ ' updated TTL is detected or build timeout has reached')
+ for ns in config.CONF.dns.nameservers:
+ start = time.time()
+ while True:
+ ns_obj = SingleQueryClient(ns, config.CONF.dns.query_timeout)
+ ns_record = ns_obj.query(
+ self.zone['name'], rdatatype=recordset_data['type'])
+ if str(updated_ttl) in str(ns_record):
+ return
+ if time.time() - start >= config.CONF.dns.build_timeout:
+ raise lib_exc.TimeoutException(
+ 'Failed, updated TTL:{} for the record was not'
+ ' detected on Nameserver:{} within a timeout of:{}'
+ ' seconds.'.format(
+ updated_ttl, ns, config.CONF.dns.build_timeout))
+
+ # These tests were unrolled from DDT to allow accurate tracking by
+ # idempotent_id's. The naming convention for the tests has been preserved.
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('cbf756b0-ba64-11ec-93d4-201e8823901f')
+ def test_update_records_propagated_to_backends_01_A(self):
+ self._test_update_records_propagated_to_backends(
+ "www", "A", ["192.0.2.1", "192.0.2.2", "192.0.2.3"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('258f7f57-9a74-4e72-bbfb-c709c411af14')
+ def test_update_records_propagated_to_backends_02_AAAA(self):
+ self._test_update_records_propagated_to_backends(
+ "www", "AAAA", ["2001:db8::1", "2001:db8::1", "2001:db8::"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('304adbc5-668a-457e-9496-8efd20b8ae82')
+ def test_update_records_propagated_to_backends_03_SRV_TCP(self):
+ self._test_update_records_propagated_to_backends(
+ "_sip._tcp", "SRV", ["10 60 5060 server1.example.com.",
+ "20 60 5060 server2.example.com.",
+ "20 30 5060 server3.example.com."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('bd1283b3-423c-4bb9-8c4f-a205f31f1c2d')
+ def test_update_records_propagated_to_backends_04_SRV_UDP(self):
+ self._test_update_records_propagated_to_backends(
+ "_sip._udp", "SRV", ["10 60 5060 server1.example.com.",
+ "10 60 5060 server2.example.com.",
+ "20 30 5060 server3.example.com."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('8b53ae20-d096-4651-a6cf-efd7c98ae8d1')
+ def test_update_records_propagated_to_backends_05_CNAME(self):
+ self._test_update_records_propagated_to_backends(
+ "alias-of-target", "CNAME", ["target.example.org."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('0fd0046a-ac5a-468d-94b3-8a6bde790589')
+ def test_update_records_propagated_to_backends_06_MX_at_APEX(self):
+ self._test_update_records_propagated_to_backends(
+ None, "MX", ["10 mail1.example.org.",
+ "20 mail2.example.org."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('31176def-3f95-459d-8bdd-b9994335b2d9')
+ def test_update_records_propagated_to_backends_07_MX_under_APEX(self):
+ self._test_update_records_propagated_to_backends(
+ "under", "MX", ["10 mail.example.org."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('0009d787-c590-4149-9f30-082195326fad')
+ def test_update_records_propagated_to_backends_08_SSHFP(self):
+ self._test_update_records_propagated_to_backends(
+ "www", "SSHFP", ["2 1 123456789abcdef67890123456789abcdef67890"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('af7cec16-dfad-4071-aa05-cafa60bf12a5')
+ def test_update_records_propagated_to_backends_09_TXT(self):
+ self._test_update_records_propagated_to_backends(
+ "www", "TXT", ["\"Any Old Text Goes Here\""])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('b3fd1f77-c318-4ab0-b18d-34611e51e9e4')
+ def test_update_records_propagated_to_backends_10_SPF(self):
+ self._test_update_records_propagated_to_backends(
+ "*.sub", "SPF", ["\"v=spf1; a -all\""])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('c310b94b-f3a5-4d26-bab6-2529e6f29fbf')
+ def test_update_records_propagated_to_backends_11_PTR_IPV4(self):
+ self._test_update_records_propagated_to_backends(
+ "PTR_Record_IPV4", "PTR", ["34.216.184.93.in-addr.arpa."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('3e31e406-621f-4f89-b401-b6f38aa63347')
+ def test_update_records_propagated_to_backends_12_PTR_IPV6(self):
+ self._test_update_records_propagated_to_backends(
+ "PTR_Record_IPV6", "PTR",
+ ["6.4.9.1.8.c.5.2.3.9.8.1.8.4.2.0.1.0.0.0.0.2.2.0.0.0.8.2.6.0.6.2"
+ ".ip6.arpa."])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('6fd96280-fb62-4eaf-81f9-609cdb7c126e')
+ def test_update_records_propagated_to_backends_13_CAA_Record(self):
+ self._test_update_records_propagated_to_backends(
+ "CAA_Record", "CAA", ["0 issue letsencrypt.org"])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('45a11efe-bee3-4896-ab7c-daee1cb5eb3a')
+ def test_update_records_propagated_to_backends_14_NAPTR_Record(self):
+ self._test_update_records_propagated_to_backends(
+ "NAPTR_Record", "NAPTR",
+ ["0 0 S SIP+D2U !^.*$!sip:customer-service@example.com! "
+ "_sip._udp.example.com."])
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_shared_zones.py b/designate_tempest_plugin/tests/scenario/v2/test_shared_zones.py
new file mode 100644
index 0000000..75aa3c3
--- /dev/null
+++ b/designate_tempest_plugin/tests/scenario/v2/test_shared_zones.py
@@ -0,0 +1,558 @@
+# Copyright 2023 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 oslo_utils import versionutils
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from designate_tempest_plugin import data_utils as dns_data_utils
+from designate_tempest_plugin.common import constants as const
+from designate_tempest_plugin.common import waiters
+from designate_tempest_plugin.tests import base
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class SharedZonesTest(base.BaseDnsV2Test):
+ credentials = ['primary', 'admin', 'system_admin', 'alt',
+ ['demo', 'member']]
+
+ @classmethod
+ def setup_clients(cls):
+ super(SharedZonesTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ cls.adm_shr_client = cls.os_system_admin.dns_v2.SharedZonesClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.adm_shr_client = cls.os_admin.dns_v2.SharedZonesClient()
+ cls.share_zone_client = cls.os_primary.dns_v2.SharedZonesClient()
+ cls.rec_client = cls.os_primary.dns_v2.RecordsetClient()
+ cls.alt_rec_client = cls.os_alt.dns_v2.RecordsetClient()
+ cls.demo_rec_client = cls.os_demo.dns_v2.RecordsetClient()
+ cls.primary_import_client = cls.os_primary.dns_v2.ZoneImportsClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(SharedZonesTest, cls).resource_setup()
+
+ if not versionutils.is_compatible('2.1', cls.api_version,
+ same_major=False):
+ raise cls.skipException(
+ 'The shared zones scenario tests require Designate API '
+ 'version 2.1 or newer. Skipping Shared Zones scenario tests.')
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name='SharedZonesTest')
+ cls.tld_name = f'.{tld_name}'
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(SharedZonesTest, cls).resource_cleanup()
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('b0fad45d-25ec-49b9-89a8-10b0e3c8b14c')
+ def test_zone_share_CRUD_recordset(self):
+ # Create a zone to share with the alt credential
+ zone_name = dns_data_utils.rand_zone_name(name='TestZone',
+ suffix=self.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+
+ # Check that the alt user has no access to the zone before the share
+ self.assertRaises(lib_exc.NotFound,
+ self.alt_rec_client.create_recordset,
+ zone['id'], recordset_data)
+
+ # Check that the demo user has no access to the zone before the share
+ self.assertRaises(lib_exc.NotFound,
+ self.demo_rec_client.create_recordset,
+ zone['id'], recordset_data)
+
+ # Share the zone with the alt credential
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.alt_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+
+ # Check that the demo user has no access to the zone after the share
+ self.assertRaises(lib_exc.NotFound,
+ self.demo_rec_client.create_recordset,
+ zone['id'], recordset_data)
+
+ # Check that the alt user can create a recordset on the shared zone
+ recordset = self.alt_rec_client.create_recordset(zone['id'],
+ recordset_data)[1]
+ self.addCleanup(self.wait_recordset_delete, self.alt_rec_client,
+ zone['id'], recordset['id'], ignore_errors=lib_exc.NotFound)
+
+ # Check that the demo user cannot see the alt recordset
+ self.assertRaises(lib_exc.NotFound,
+ self.demo_rec_client.show_recordset,
+ zone['id'], recordset['id'])
+
+ # Check that the alt user can see the alt recordset
+ show_recordset = self.alt_rec_client.show_recordset(
+ zone['id'], recordset['id'])[1]
+
+ self.assertEqual(recordset['id'], show_recordset['id'])
+
+ # Check that the zone owner can see the alt recordset
+ show_recordset = self.rec_client.show_recordset(zone['id'],
+ recordset['id'])[1]
+
+ self.assertEqual(recordset['id'], show_recordset['id'])
+
+ recordset_data = {
+ 'ttl': dns_data_utils.rand_ttl(start=recordset['ttl'] + 1)
+ }
+
+ # Check that the demo user cannot update the recordset created by alt
+ self.assertRaises(lib_exc.NotFound,
+ self.demo_rec_client.update_recordset,
+ zone['id'], recordset['id'], recordset_data)
+
+ # Check that the alt user can update a recordset on the shared zone
+ update = self.alt_rec_client.update_recordset(zone['id'],
+ recordset['id'], recordset_data)[1]
+
+ self.assertNotEqual(recordset['ttl'], update['ttl'])
+
+ recordset_data = {
+ 'ttl': dns_data_utils.rand_ttl(start=update['ttl'] + 1)
+ }
+
+ # Check that the zone owner can update a recordset on the shared zone
+ primary_update = self.rec_client.update_recordset(zone['id'],
+ recordset['id'], recordset_data)[1]
+
+ self.assertNotEqual(update['ttl'], primary_update['ttl'])
+
+ # Check that the demo user cannot delete the alt recordset
+ self.assertRaises(lib_exc.NotFound,
+ self.demo_rec_client.delete_recordset,
+ zone['id'], recordset['id'])
+
+ # Check that the alt user can delete it's recordset
+ self.alt_rec_client.delete_recordset(zone['id'], recordset['id'])
+
+ LOG.info('Ensure successful deletion of Recordset')
+ self.assertRaises(lib_exc.NotFound,
+ self.alt_rec_client.show_recordset,
+ zone['id'], recordset['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('de03b4d3-3ccf-4291-a920-89e2694bba22')
+ def test_zone_owner_can_delete_shared_recordset(self):
+ # Create a zone to share with the alt credential
+ zone_name = dns_data_utils.rand_zone_name(name='TestZone',
+ suffix=self.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+
+ # Share the zone with the alt credential
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.alt_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+
+ # Check that the alt user can create a recordset on the shared zone
+ recordset = self.alt_rec_client.create_recordset(zone['id'],
+ recordset_data)[1]
+ self.addCleanup(self.wait_recordset_delete, self.alt_rec_client,
+ zone['id'], recordset['id'], ignore_errors=lib_exc.NotFound)
+
+ # Check that the alt user can see the alt recordset
+ show_recordset = self.alt_rec_client.show_recordset(
+ zone['id'], recordset['id'])[1]
+
+ self.assertEqual(recordset['id'], show_recordset['id'])
+
+ # Check that the zone owner can delete the recordset
+ self.rec_client.delete_recordset(zone['id'], recordset['id'])
+
+ LOG.info('Ensure successful deletion of Recordset')
+ self.assertRaises(lib_exc.NotFound,
+ self.alt_rec_client.show_recordset,
+ zone['id'], recordset['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('fb170be8-c0bc-11ed-99a3-201e8823901f')
+ def test_admin_zone_share_CRUD_recordset(self):
+
+ # Create a zone to share with the alt credential
+ zone_name = dns_data_utils.rand_zone_name(name='TestZone',
+ suffix=self.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ # Generate recordset data to be used latter in the test
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+
+ # Check that the alt user has no access to the zone before the share
+ self.assertRaises(lib_exc.NotFound,
+ self.alt_rec_client.create_recordset,
+ zone['id'], recordset_data)
+
+ # Admin creates shared zone for Alt using "x-auth-sudo-project-id"
+ sudo_header = {
+ 'x-auth-sudo-project-id': self.zones_client.project_id}
+ shared_zone = self.adm_shr_client.create_zone_share(
+ zone['id'], self.alt_rec_client.project_id,
+ headers=sudo_header)[1]
+ self.addCleanup(
+ self.adm_shr_client.delete_zone_share, zone['id'],
+ shared_zone['id'], headers=sudo_header)
+
+ # Check that the alt user can create a recordset on the shared zone
+ recordset = self.alt_rec_client.create_recordset(zone['id'],
+ recordset_data)[1]
+ self.addCleanup(self.wait_recordset_delete, self.alt_rec_client,
+ zone['id'], recordset['id'], ignore_errors=lib_exc.NotFound)
+
+ # Check that the alt user can see the alt recordset
+ show_recordset = self.alt_rec_client.show_recordset(
+ zone['id'], recordset['id'])[1]
+ self.assertEqual(recordset['id'], show_recordset['id'])
+
+ # Check that the zone owner can see the alt recordset
+ show_recordset = self.rec_client.show_recordset(
+ zone['id'], recordset['id'])[1]
+ self.assertEqual(recordset['id'], show_recordset['id'])
+ recordset_data = {
+ 'ttl': dns_data_utils.rand_ttl(start=recordset['ttl'] + 1)
+ }
+
+ # Check that the alt user can update a recordset on the shared zone
+ update = self.alt_rec_client.update_recordset(zone['id'],
+ recordset['id'], recordset_data)[1]
+ self.assertNotEqual(recordset['ttl'], update['ttl'])
+ recordset_data = {
+ 'ttl': dns_data_utils.rand_ttl(start=update['ttl'] + 1)
+ }
+
+ # Check that the zone owner can update a recordset on the shared zone
+ primary_update = self.rec_client.update_recordset(zone['id'],
+ recordset['id'], recordset_data)[1]
+ self.assertNotEqual(update['ttl'], primary_update['ttl'])
+
+ # Check that the alt user can delete it's recordset
+ self.alt_rec_client.delete_recordset(zone['id'], recordset['id'])
+ LOG.info('Ensure successful deletion of Recordset')
+ self.assertRaises(lib_exc.NotFound,
+ self.alt_rec_client.show_recordset,
+ zone['id'], recordset['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('b7dd37b8-c3ea-11ed-a102-201e8823901f')
+ def test_share_imported_zone(self):
+ # Primary user imports zone from a zone file
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_share_imported_zone", suffix=self.tld_name)
+ zone_data = dns_data_utils.rand_zonefile_data(name=zone_name)
+ zone_import = self.primary_import_client.create_zone_import(
+ zonefile_data=zone_data)[1]
+ self.addCleanup(
+ self.primary_import_client.delete_zone_import, zone_import['id'])
+ waiters.wait_for_zone_import_status(
+ self.primary_import_client, zone_import['id'], const.COMPLETE)
+
+ # Primary shares previously created zone with Alt user
+ zone_id = self.primary_import_client.show_zone_import(
+ zone_import['id'])[1]['zone_id']
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone_id, self.alt_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone_id, shared_zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('c5d83684-18cb-11ee-a872-201e8823901f')
+ def test_list_zones_shared_with_more_then_two_projects(self):
+ # Create a zone to share with the alt credentialzones_client
+ zone_name = dns_data_utils.rand_zone_name(name='TestZone',
+ suffix=self.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ zone_id = zone['id']
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ # Share the zone with the alt credential
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.alt_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+
+ # Share the zone with the demo credential
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.demo_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+
+ zones = self.zones_client.list_zones()[1]['zones']
+ zones_ids = [zone['id'] for zone in zones]
+ self.assertEqual(
+ 1, zones_ids.count(zone_id),
+ 'Failed, ID:{} counted in zones listed:{} must be one'.format(
+ zone_id, zones))
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('78b77c6c-18cf-11ee-a872-201e8823901f')
+ def test_create_recordset_for_zone_shared_with_two_projects(self):
+ # Create a zone to share with the alt credential
+ zone_name = dns_data_utils.rand_zone_name(name='TestZone',
+ suffix=self.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ # Share the zone with the alt credential
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.alt_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+
+ # Check that the alt user can create a recordset on the shared zone
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ recordset = self.alt_rec_client.create_recordset(
+ zone['id'], recordset_data)[1]
+ self.addCleanup(self.wait_recordset_delete, self.alt_rec_client,
+ zone['id'], recordset['id'], ignore_errors=lib_exc.NotFound)
+
+ # Share the zone with the demo credential
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.demo_rec_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+
+ # Check that the demo user can create a recordset on the shared zone
+ recordset_data = dns_data_utils.rand_recordset_data(
+ record_type='A', zone_name=zone['name'])
+ recordset = self.demo_rec_client.create_recordset(
+ zone['id'], recordset_data)[1]
+ self.addCleanup(self.wait_recordset_delete, self.demo_rec_client,
+ zone['id'], recordset['id'], ignore_errors=lib_exc.NotFound)
+
+
+class SharedZonesTestNegative(base.BaseDnsV2Test):
+ credentials = ['primary', 'admin', 'system_admin', 'alt',
+ ['demo', 'member']]
+
+ @classmethod
+ def setup_clients(cls):
+ super(SharedZonesTestNegative, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ cls.adm_shr_client = cls.os_system_admin.dns_v2.SharedZonesClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.adm_shr_client = cls.os_admin.dns_v2.SharedZonesClient()
+ cls.share_zone_client = cls.os_primary.dns_v2.SharedZonesClient()
+ cls.alt_export_client = cls.os_alt.dns_v2.ZoneExportsClient()
+ cls.primary_export_client = cls.os_primary.dns_v2.ZoneExportsClient()
+ cls.alt_zone_client = cls.os_alt.dns_v2.ZonesClient()
+ cls.primary_import_client = cls.os_primary.dns_v2.ZoneImportsClient()
+ cls.alt_import_client = cls.os_alt.dns_v2.ZoneImportsClient()
+ cls.prm_transfer_client = cls.os_primary.dns_v2.TransferRequestClient()
+ cls.alt_transfer_client = cls.os_alt.dns_v2.TransferRequestClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(SharedZonesTestNegative, cls).resource_setup()
+ if not versionutils.is_compatible('2.1', cls.api_version,
+ same_major=False):
+ raise cls.skipException(
+ 'The shared zones scenario tests require Designate API '
+ 'version 2.1 or newer. Skipping Shared Zones scenario tests.')
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name='SharedZonesTest')
+ cls.tld_name = f'.{tld_name}'
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(SharedZonesTestNegative, cls).resource_cleanup()
+
+ def _create_shared_zone(self, zone_name):
+ # Primary tenant creates zone and shares it with Alt tenant
+ zone_name = dns_data_utils.rand_zone_name(
+ name=zone_name, suffix=self.tld_name)
+ LOG.info('Create a zone: %s', zone_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+ shared_zone = self.share_zone_client.create_zone_share(
+ zone['id'], self.alt_export_client.project_id)[1]
+ self.addCleanup(self.share_zone_client.delete_zone_share,
+ zone['id'], shared_zone['id'])
+ return zone, shared_zone
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('1d2c91c2-c328-11ed-a033-201e8823901f')
+ def test_alt_create_export_for_shared_zone(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_create_export_for_shared_zone')[0]
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.alt_export_client.create_zone_export, zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('1e74410c-c32c-11ed-a033-201e8823901f')
+ def test_alt_list_shared_zone_exports(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_list_shared_zone_exports')[0]
+
+ # Primary creates zone export
+ zone_export = self.primary_export_client.create_zone_export(
+ zone['id'])[1]
+ self.addCleanup(
+ self.primary_export_client.delete_zone_export, zone_export['id'])
+ waiters.wait_for_zone_export_status(
+ self.primary_export_client, zone_export['id'], const.COMPLETE)
+
+ # Primary lists zone exports
+ prim_zone_exports = self.primary_export_client.list_zone_exports()[1]
+ self.assertEqual(1, len(prim_zone_exports['exports']),
+ 'Failed, no zone exports listed for a primary tenant')
+
+ # Alt tries to list Primary's zone exports
+ alt_zone_exports = self.alt_export_client.list_zone_exports()[1]
+ self.assertEqual(
+ 0, len(alt_zone_exports['exports']),
+ 'Failed, Alt tenant is expected to receive an '
+ 'empty list of zone exports')
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('cac8ea8e-c33b-11ed-a033-201e8823901f')
+ def test_alt_delete_shared_zone_export(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_delete_shared_zone_export')[0]
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.alt_export_client.delete_zone_export, zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('962df772-c33d-11ed-a033-201e8823901f')
+ def test_alt_fails_to_show_exported_zonefile_for_shared_zone(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_show_exported_zonefile_for_shared_zone')[0]
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.alt_export_client.show_exported_zonefile, zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('089136f2-c3e4-11ed-a102-201e8823901f')
+ def test_alt_shows_shared_zones_nameservers(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_shows_shared_zones_nameservers')[0]
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.alt_zone_client.show_zone_nameservers, zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('6376d4ca-c3f6-11ed-a102-201e8823901f')
+ def test_alt_transfers_shared_zone(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_transfers_shared_zone')[0]
+ # Alt creates a zone transfer_request
+ self.assertRaises(
+ lib_exc.Forbidden,
+ self.alt_transfer_client.create_transfer_request, zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('80ffbd8a-c3f7-11ed-a102-201e8823901f')
+ def test_alt_show_delete_transfers_of_shared_zone(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_show_delete_transfers_of_shared_zone')[0]
+
+ # Primary user creates a zone transfer_request
+ transfer_request = self.prm_transfer_client.create_transfer_request(
+ zone['id'])[1]
+ self.addCleanup(
+ self.prm_transfer_client.delete_transfer_request,
+ transfer_request['id'])
+ self.assertEqual('ACTIVE', transfer_request['status'])
+
+ # Alt shows a zone transfer_request
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.alt_transfer_client.show_transfer_request, zone['id'])
+
+ # Alt deletes a zone transfer_request
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.alt_transfer_client.delete_transfer_request, zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('1da0ff64-c3f8-11ed-a102-201e8823901f')
+ def test_alt_lists_transfers_of_shared_zone(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_lists_transfers_of_shared_zone')[0]
+
+ # Primary user creates a zone transfer_request
+ transfer = self.prm_transfer_client.create_transfer_request(
+ zone['id'])[1]
+ self.addCleanup(self.prm_transfer_client.delete_transfer_request,
+ transfer['id'])
+ self.assertEqual('ACTIVE', transfer['status'])
+ transfer = self.prm_transfer_client.list_transfer_requests()[1]
+ self.assertEqual(
+ 1, len(transfer['transfer_requests']),
+ 'Failed, there is no transfer request listed for a primary user')
+
+ # Alt user lists shared zone transfer requests
+ transfer = self.alt_transfer_client.list_transfer_requests()[1]
+ self.assertEqual(
+ 0, len(transfer['transfer_requests']),
+ 'Failed, transfer request list should be empty for for Alt user')
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('1702c1d6-c643-11ed-8d86-201e8823901f')
+ def test_alt_abandon_shared_zone(self):
+ # Primary creates Zone and shares it with Alt
+ zone = self._create_shared_zone(
+ 'test_alt_lists_transfers_of_shared_zone')[0]
+ self.assertRaises(
+ lib_exc.Forbidden, self.alt_zone_client.abandon_zone,
+ zone['id'])
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_tld.py b/designate_tempest_plugin/tests/scenario/v2/test_tld.py
new file mode 100644
index 0000000..bc90c90
--- /dev/null
+++ b/designate_tempest_plugin/tests/scenario/v2/test_tld.py
@@ -0,0 +1,79 @@
+# 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.common import constants as const
+from designate_tempest_plugin.tests import base
+from designate_tempest_plugin import data_utils as dns_data_utils
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class TldZoneTest(base.BaseDnsV2Test):
+ credentials = ["admin", "system_admin", "primary"]
+ tld_suffix = '.'.join(["TldZoneTest", CONF.dns.tld_suffix])
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(TldZoneTest, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ super(TldZoneTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.primary_tld_client = cls.os_primary.dns_v2.TldClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(TldZoneTest, cls).resource_setup()
+ cls.class_tld = cls.admin_tld_client.create_tld(
+ tld_name=cls.tld_suffix)
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(TldZoneTest, cls).resource_cleanup()
+
+ @decorators.idempotent_id('68b3e7cc-bf0e-11ec-b803-201e8823901f')
+ def test_create_zone_using_existing_tld(self):
+ LOG.info('Creates a zone using existing TLD:"{}"'.format(
+ self.tld_suffix))
+ zone_name = dns_data_utils.rand_zone_name(
+ name='existing_tld_zone', prefix='rand',
+ suffix='.{}.'.format(self.tld_suffix))
+ zone = self.zones_client.create_zone(
+ name=zone_name, wait_until=const.ACTIVE)[1]
+ self.addCleanup(
+ self.wait_zone_delete, self.zones_client, zone['id'])
+
+ @decorators.idempotent_id('06deced8-d4de-11eb-b8ee-74e5f9e2a801')
+ def test_create_zone_using_not_existing_tld(self):
+ LOG.info('Try to create a Zone using not existing TLD:"{}"'.format(
+ self.tld_suffix[::-1]))
+ zone_name = dns_data_utils.rand_zone_name(
+ name='not_existing_tld_zone', prefix='rand',
+ suffix='.{}.'.format(self.tld_suffix)[::-1])
+ self.assertRaises(
+ lib_exc.BadRequest, self.zones_client.create_zone,
+ name=zone_name)
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_zones.py b/designate_tempest_plugin/tests/scenario/v2/test_zones.py
index a7529a1..98b2f9c 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_zones.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_zones.py
@@ -11,64 +11,134 @@
# 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 time
+import math
+
from oslo_log import log as logging
from tempest import config
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
import testtools
+from designate_tempest_plugin import data_utils as dns_data_utils
from designate_tempest_plugin.tests import base
+from designate_tempest_plugin.common import constants as const
from designate_tempest_plugin.common import waiters
+from designate_tempest_plugin.services.dns.query.query_client import (
+ SingleQueryClient)
+CONF = config.CONF
+CONF = config.CONF
LOG = logging.getLogger(__name__)
class ZonesTest(base.BaseDnsV2Test):
+ credentials = ["primary", "admin", "system_admin"]
+
@classmethod
def setup_clients(cls):
super(ZonesTest, cls).setup_clients()
+ if CONF.enforce_scope.designate:
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ cls.rec_client = cls.os_system_admin.dns_v2.RecordsetClient()
+ else:
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
+ cls.rec_client = cls.os_admin.dns_v2.RecordsetClient()
+ cls.primary_client = cls.os_primary.dns_v2.BlacklistsClient()
- cls.client = cls.os_primary.dns_v2.ZonesClient()
+ @classmethod
+ def resource_setup(cls):
+ super(ZonesTest, cls).resource_setup()
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="ZonesTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(ZonesTest, cls).resource_cleanup()
@decorators.attr(type='smoke')
@decorators.attr(type='slow')
@decorators.idempotent_id('d0648f53-4114-45bd-8792-462a82f69d32')
def test_create_and_delete_zone(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'],
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_and_delete_zone", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
ignore_errors=lib_exc.NotFound)
LOG.info('Ensure we respond with CREATE+PENDING')
- self.assertEqual('CREATE', zone['action'])
- self.assertEqual('PENDING', zone['status'])
+ self.assertEqual(const.CREATE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
waiters.wait_for_zone_status(
- self.client, zone['id'], 'ACTIVE')
+ self.zones_client, zone['id'], const.ACTIVE)
LOG.info('Re-Fetch the zone')
- _, zone = self.client.show_zone(zone['id'])
+ zone = self.zones_client.show_zone(zone['id'])[1]
- LOG.info('Ensure we respond with NONE+PENDING')
- self.assertEqual('NONE', zone['action'])
- self.assertEqual('ACTIVE', zone['status'])
+ LOG.info('Ensure we respond with NONE+ACTIVE')
+ self.assertEqual(const.NONE, zone['action'])
+ self.assertEqual(const.ACTIVE, zone['status'])
LOG.info('Delete the zone')
- _, zone = self.client.delete_zone(zone['id'])
+ zone = self.zones_client.delete_zone(zone['id'])[1]
LOG.info('Ensure we respond with DELETE+PENDING')
- self.assertEqual('DELETE', zone['action'])
- self.assertEqual('PENDING', zone['status'])
+ self.assertEqual(const.DELETE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
- waiters.wait_for_zone_404(self.client, zone['id'])
+ waiters.wait_for_zone_404(self.zones_client, zone['id'])
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('cabd6334-ba37-11ec-9d8c-201e8823901f')
+ def test_create_and_update_zone(self):
+
+ LOG.info('Create a zone and wait until it becomes ACTIVE')
+ orig_ttl = 666
+ orig_description = 'test_create_and_update_zone: org description'
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_and_update_zone", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(
+ name=zone_name,
+ ttl=orig_ttl, description=orig_description,
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ LOG.info("Update zone's: TTL and Description, wait until ACTIVE")
+ updated_ttl = 777
+ updated_description = dns_data_utils.rand_string(20)
+ self.zones_client.update_zone(
+ zone['id'], ttl=updated_ttl, description=updated_description,
+ wait_until=const.ACTIVE)
+
+ LOG.info('Re-Fetch/Show the zone')
+ show_zone = self.zones_client.show_zone(zone['id'])[1]
+
+ LOG.info('Ensure that the Description and TLL has been updated')
+ self.assertEqual(
+ updated_ttl, show_zone['ttl'],
+ 'Failed, actual TTL value:{} is not as expected:{} after '
+ 'the update)'.format(show_zone['ttl'], updated_ttl))
+ self.assertEqual(
+ updated_description, show_zone['description'],
+ 'Failed, actual Description:{} is not as expected:{} after '
+ 'the update)'.format(show_zone['description'], orig_description))
@decorators.attr(type='slow')
@decorators.idempotent_id('c9838adf-14dc-4097-9130-e5cea3727abb')
def test_delete_zone_pending_create(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'],
+ zone_name = dns_data_utils.rand_zone_name(
+ name="delete_zone_pending_create", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
ignore_errors=lib_exc.NotFound)
# NOTE(kiall): This is certainly a little racey, it's entirely
@@ -77,13 +147,13 @@
# Theres not a huge amount we can do, given this is
# black-box testing.
LOG.info('Delete the zone while it is still pending')
- _, zone = self.client.delete_zone(zone['id'])
+ zone = self.zones_client.delete_zone(zone['id'])[1]
LOG.info('Ensure we respond with DELETE+PENDING')
- self.assertEqual('DELETE', zone['action'])
- self.assertEqual('PENDING', zone['status'])
+ self.assertEqual(const.DELETE, zone['action'])
+ self.assertEqual(const.PENDING, zone['status'])
- waiters.wait_for_zone_404(self.client, zone['id'])
+ waiters.wait_for_zone_404(self.zones_client, zone['id'])
@decorators.attr(type='slow')
@decorators.idempotent_id('ad8d1f5b-da66-46a0-bbee-14dc84a5d791')
@@ -92,11 +162,14 @@
"Config option dns.nameservers is missing or empty")
def test_zone_create_propagates_to_nameservers(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'])
+ zone_name = dns_data_utils.rand_zone_name(
+ name="zone_create_propagates", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
- waiters.wait_for_zone_status(self.client, zone['id'], "ACTIVE")
- waiters.wait_for_query(self.query_client, zone['name'], "SOA")
+ waiters.wait_for_zone_status(self.zones_client, zone['id'],
+ const.ACTIVE)
+ waiters.wait_for_query(self.query_client, zone['name'], const.SOA)
@decorators.attr(type='slow')
@decorators.idempotent_id('d13d3095-c78f-4aae-8fe3-a74ccc335c84')
@@ -105,16 +178,83 @@
"Config option dns.nameservers is missing or empty")
def test_zone_delete_propagates_to_nameservers(self):
LOG.info('Create a zone')
- _, zone = self.client.create_zone()
- self.addCleanup(self.wait_zone_delete, self.client, zone['id'],
+ zone_name = dns_data_utils.rand_zone_name(
+ name="zone_delete_propagates", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, 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")
+ waiters.wait_for_zone_status(self.zones_client, zone['id'],
+ const.ACTIVE)
+ waiters.wait_for_query(self.query_client, zone['name'], const.SOA)
LOG.info('Delete the zone')
- self.client.delete_zone(zone['id'])
+ self.zones_client.delete_zone(zone['id'])
- waiters.wait_for_zone_404(self.client, zone['id'])
- waiters.wait_for_query(self.query_client, zone['name'], "SOA",
+ waiters.wait_for_zone_404(self.zones_client, zone['id'])
+ waiters.wait_for_query(self.query_client, zone['name'], const.SOA,
found=False)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('ff9b9fc4-85b4-11ec-bcf5-201e8823901f')
+ @testtools.skipUnless(
+ config.CONF.dns.nameservers,
+ "Config option dns.nameservers is missing or empty")
+ def test_notify_msg_sent_to_nameservers(self):
+
+ # Test will only run when the SOA record Refresh is close to one hour,
+ # otherwise skipped.
+ # This implies that the only reason "A" record was propagated is as a
+ # result of successfully sent NOTIFY message.
+
+ LOG.info('Create a zone, wait until ACTIVE and get the Serial'
+ ' and SOA Refresh values')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="test_notify_msg_sent_to_nameservers", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until='ACTIVE')[1]
+
+ org_serial = zone['serial']
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+ try:
+ soa = [
+ rec['records'] for rec in self.rec_client.list_recordset(
+ zone['id'], headers=self.all_projects_header)[1][
+ 'recordsets'] if rec['type'] == 'SOA'][0]
+ refresh = int(soa[0].split(' ')[3])
+ if math.isclose(3600, refresh, rel_tol=0.1) is False:
+ raise self.skipException(
+ 'Test is skipped, actual SOA REFRESH is:{} unlike test'
+ ' prerequisites that requires a value close to 3600'
+ ' (one hour)'.format(refresh))
+ except Exception as e:
+ raise self.skipException(
+ 'Test is skipped, something went wrong on getting SOA REFRESH'
+ ' value, the error was:{}'.format(e))
+
+ LOG.info("Update Zone's TTL, wait until ACTIVE and"
+ " ensure Zone's Serial has changed")
+ self.zones_client.update_zone(
+ zone['id'], ttl=dns_data_utils.rand_ttl(), wait_until='ACTIVE')
+ new_serial = self.zones_client.show_zone(zone['id'])[1]['serial']
+ self.assertNotEqual(
+ new_serial, org_serial,
+ "Failed, expected behaviour is that the Designate DNS changes the"
+ " Serial after updating Zone's TTL value")
+ waiters.wait_for_query(self.query_client, zone['name'], "SOA")
+
+ LOG.info('Per Nameserver "dig" for a SOA record until either:'
+ ' updated Serial is detected or build timeout has reached')
+ for ns in config.CONF.dns.nameservers:
+ start = time.time()
+ while True:
+ ns_obj = SingleQueryClient(ns, config.CONF.dns.query_timeout)
+ ns_soa_record = ns_obj.query(zone['name'], rdatatype='SOA')
+ if str(new_serial) in str(ns_soa_record):
+ return
+ if time.time() - start >= config.CONF.dns.build_timeout:
+ raise lib_exc.TimeoutException(
+ 'Failed, expected Serial:{} for a Zone was not'
+ ' detected on Nameserver:{} within a timeout of:{}'
+ ' seconds.'.format(
+ new_serial, ns, config.CONF.dns.build_timeout))
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_zones_export.py b/designate_tempest_plugin/tests/scenario/v2/test_zones_export.py
index 916e634..8c8d674 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_zones_export.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_zones_export.py
@@ -12,49 +12,70 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
+import os
+
from oslo_log import log as logging
+from tempest import config
from tempest.lib import decorators
+from designate_tempest_plugin.common import constants as const
from designate_tempest_plugin.common import waiters
-from designate_tempest_plugin.tests.api.v2.test_zones_exports import \
- BaseZoneExportsTest
+from designate_tempest_plugin import data_utils as dns_data_utils
+from designate_tempest_plugin.tests.api.v2.test_zones_exports import (
+ BaseZoneExportsTest)
+CONF = config.CONF
LOG = logging.getLogger(__name__)
class ZonesExportTest(BaseZoneExportsTest):
+ credentials = ["primary", "admin", "system_admin"]
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not create network resources for these test.
+ cls.set_network_resources()
+ super(ZonesExportTest, cls).setup_credentials()
@classmethod
def setup_clients(cls):
super(ZonesExportTest, cls).setup_clients()
-
+ if CONF.enforce_scope.designate:
+ cls.admin_client = cls.os_system_admin.dns_v2.ZoneExportsClient()
+ else:
+ cls.admin_client = cls.os_admin.dns_v2.ZoneExportsClient()
cls.client = cls.os_primary.dns_v2.ZoneExportsClient()
- cls.zones_client = cls.os_primary.dns_v2.ZonesClient()
+ cls.recordset_client = cls.os_primary.dns_v2.RecordsetClient()
- @decorators.attr(type='slow')
- @decorators.idempotent_id('0484c3c4-df57-458e-a6e5-6eb63e0475e0')
- def test_create_zone_export_and_show_exported_zonefile(self):
- LOG.info('Create a zone to be exported')
- _, zone = self.zones_client.create_zone()
+ def _create_zone_export(self, test_name):
+ LOG.info('Create a zone')
+ zone_name = dns_data_utils.rand_zone_name(
+ name=test_name, suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
LOG.info('Create a zone export')
- _, zone_export = self.client.create_zone_export(zone['id'])
+ zone_export = self.client.create_zone_export(
+ zone['id'], wait_until=const.COMPLETE)[1]
self.addCleanup(self.client.delete_zone_export, zone_export['id'])
- self.assertEqual('PENDING', zone_export['status'])
+ return zone, zone_export
+
+ @decorators.idempotent_id('0484c3c4-df57-458e-a6e5-6eb63e0475e0')
+ def test_create_zone_export_and_show_exported_zonefile(self):
+ zone, zone_export = self._create_zone_export(
+ 'create_zone_export_and_show_exported_zonefile')
+
+ self.assertEqual(const.PENDING, zone_export['status'])
self.assertEqual(zone['id'], zone_export['zone_id'])
self.assertIsNone(zone_export['links'].get('export'))
self.assertIsNone(zone_export['location'])
- LOG.info('Wait for the zone export to COMPLETE')
- waiters.wait_for_zone_export_status(
- self.client, zone_export['id'], 'COMPLETE')
-
LOG.info('Check the zone export looks good')
_, zone_export = self.client.show_zone_export(zone_export['id'])
- self.assertEqual('COMPLETE', zone_export['status'])
+ self.assertEqual(const.COMPLETE, zone_export['status'])
self.assertEqual(zone['id'], zone_export['zone_id'])
self.assertIsNotNone(zone_export['links'].get('export'))
self.assertIsNotNone(zone_export['location'])
@@ -63,3 +84,116 @@
_, zonefile = self.client.show_exported_zonefile(zone_export['id'])
self.assertEqual(zone['name'], zonefile.origin)
self.assertEqual(zone['ttl'], zonefile.ttl)
+
+ @decorators.idempotent_id('56b8f30e-cd45-4c7a-bc0c-bbf92d7dc697')
+ def test_show_exported_zonefile_impersonate_another_project(self):
+ zone, zone_export = self._create_zone_export(
+ 'show_exported_zonefile_impersonate')
+
+ LOG.info('As Admin impersonate "primary" client,'
+ ' to show exported zone file')
+ response = self.admin_client.show_exported_zonefile(
+ zone_export['id'], headers={
+ 'x-auth-sudo-project-id': zone['project_id']})[1]
+ self.assertEqual(zone['name'], response.origin)
+ self.assertEqual(zone['ttl'], response.ttl)
+
+ @decorators.idempotent_id('c2e55514-ff2e-41d9-a3cc-9e78873254c9')
+ def test_show_exported_zonefile_all_projects(self):
+ zone, zone_export = self._create_zone_export(
+ 'show_exported_zonefile_all_projects')
+ resp_headers, resp_data = self.admin_client.show_exported_zonefile(
+ zone_export['id'], headers={
+ 'x-auth-all-projects': True
+ })
+ self.assertEqual(zone['name'], resp_data.origin)
+ self.assertEqual(zone['ttl'], resp_data.ttl)
+
+ @decorators.idempotent_id('9746b7f2-2df4-448c-8a85-5ab6bf74f1fe')
+ def test_show_exported_zonefile_any_mime_type(self):
+ zone, zone_export = self._create_zone_export(
+ 'show_exported_zonefile_any_mime_type')
+ resp_headers, resp_data = self.client.show_exported_zonefile(
+ zone_export['id'], headers={'Accept': '*/*'})
+
+ LOG.info('Ensure Content-Type: text/dns')
+ self.assertIn(
+ 'text/dns', resp_headers['content-type'],
+ "Failed, the expected 'Content-type:text/dns wasn't received.")
+
+ LOG.info('Ensure exported data ia as expected')
+ self.assertEqual(zone['name'], resp_data.origin)
+ self.assertEqual(zone['ttl'], resp_data.ttl)
+
+ @decorators.idempotent_id('dc7a9dde-d287-4e22-9788-26578f0d3bf0')
+ def test_missing_accept_headers(self):
+ zone, zone_export = self._create_zone_export(
+ 'missing_accept_headers')
+ resp_headers, resp_data = self.client.show_exported_zonefile(
+ zone_export['id'], headers={})
+ LOG.info('Ensure Content-Type: text/dns')
+ self.assertIn(
+ 'text/dns', resp_headers['content-type'],
+ "Failed, the expected 'Content-type:text/dns wasn't received.")
+
+ LOG.info('Ensure exported data ia as expected')
+ self.assertEqual(zone['name'], resp_data.origin)
+ self.assertEqual(zone['ttl'], resp_data.ttl)
+
+ @decorators.attr(type='slow')
+ @decorators.idempotent_id('d8f444aa-a645-4a03-b366-46836f57dc69')
+ def test_all_recordset_types_exist_in_show_zonefile(self):
+ recorsets_data_file = os.path.join(
+ os.path.dirname(__file__), 'recordset_data.json')
+
+ if not os.path.exists(recorsets_data_file):
+ raise self.skipException(
+ f"Could not find {recorsets_data_file}")
+
+ file = open(recorsets_data_file, "r")
+ load_file = json.loads(file.read())
+ file.close()
+
+ LOG.info('Create a zone')
+ zone_name = dns_data_utils.rand_zone_name(
+ name="all_recordset_types_exist", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name,
+ wait_until=const.ACTIVE)[1]
+ self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'])
+
+ created_records = []
+ for record_data in load_file.values():
+ recordset_data = {
+ 'name': f"{record_data['name']}.{zone['name']}",
+ 'type': record_data['type'],
+ 'records': record_data['records'],
+ }
+ try:
+ LOG.info('Create a Recordset')
+ recordset = self.recordset_client.create_recordset(
+ zone['id'], recordset_data)[1]
+ self.addCleanup(self.wait_recordset_delete,
+ self.recordset_client, zone['id'],
+ recordset['id'])
+ created_records.append(recordset['records'])
+ waiters.wait_for_recordset_status(self.recordset_client,
+ zone['id'], recordset['id'],
+ const.ACTIVE)
+ except Exception as err:
+ LOG.warning(f"Record of type {recordset['type']} could not be"
+ f" created and failed with error: {err}")
+
+ LOG.info('Create a zone export')
+ zone_export = self.client.create_zone_export(
+ zone['id'], wait_until=const.COMPLETE)[1]
+ self.addCleanup(self.client.delete_zone_export, zone_export['id'])
+
+ LOG.info('Show exported zonefile')
+ created_zonefile = self.client.show_exported_zonefile(
+ zone_export['id'])[1]
+
+ file_records = [item.data for item in created_zonefile.records]
+ for record in created_records:
+ for r in record:
+ self.assertIn(r, file_records,
+ f"Failed, missing record: {r} in zone file")
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_zones_import.py b/designate_tempest_plugin/tests/scenario/v2/test_zones_import.py
index e92feee..9518d82 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_zones_import.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_zones_import.py
@@ -14,53 +14,53 @@
from oslo_log import log as logging
from tempest.lib import decorators
+from designate_tempest_plugin.common import constants as const
from designate_tempest_plugin.common import waiters
from designate_tempest_plugin import data_utils as dns_data_utils
-from designate_tempest_plugin.tests.api.v2.test_zones_imports import \
- BaseZonesImportTest
+from designate_tempest_plugin.tests.api.v2.test_zones_imports import (
+ BaseZonesImportTest)
LOG = logging.getLogger(__name__)
class ZonesImportTest(BaseZonesImportTest):
+ credentials = ["primary", "admin", "system_admin"]
+
@classmethod
def setup_clients(cls):
super(ZonesImportTest, cls).setup_clients()
cls.client = cls.os_primary.dns_v2.ZoneImportsClient()
- cls.zones_client = cls.os_primary.dns_v2.ZonesClient()
@decorators.attr(type='slow')
@decorators.idempotent_id('679f38d0-2f2f-49c5-934e-8fe0c452f56e')
def test_create_zone_import_and_wait_for_zone(self):
- name = dns_data_utils.rand_zone_name('testimport')
- zonefile = dns_data_utils.rand_zonefile_data(name=name)
+ zone_name = dns_data_utils.rand_zone_name(
+ name="create_zone_import_and_wait_for_zone", suffix=self.tld_name)
+ zonefile = dns_data_utils.rand_zonefile_data(name=zone_name)
- LOG.info('Import zone %r', name)
- _, zone_import = self.client.create_zone_import(zonefile)
+ LOG.info('Import zone %r', zone_name)
+ zone_import = self.client.create_zone_import(
+ zonefile, wait_until=const.COMPLETE)[1]
self.addCleanup(self.client.delete_zone_import, zone_import['id'])
- LOG.info('Wait for the zone import to COMPLETE')
- waiters.wait_for_zone_import_status(self.client, zone_import['id'],
- "COMPLETE")
-
LOG.info('Check the zone import looks good')
- _, zone_import = self.client.show_zone_import(zone_import['id'])
+ zone_import = self.client.show_zone_import(zone_import['id'])[1]
self.addCleanup(self.wait_zone_delete,
self.zones_client,
zone_import['zone_id'])
- self.assertEqual('COMPLETE', zone_import['status'])
+ self.assertEqual(const.COMPLETE, zone_import['status'])
self.assertIsNotNone(zone_import['zone_id'])
self.assertIsNotNone(zone_import['links'].get('zone'))
LOG.info('Wait for the imported zone to go to ACTIVE')
- waiters.wait_for_zone_status(self.zones_client, zone_import['zone_id'],
- "ACTIVE")
+ waiters.wait_for_zone_status(
+ self.zones_client, zone_import['zone_id'], const.ACTIVE)
LOG.info('Check the imported zone looks good')
- _, zone = self.zones_client.show_zone(zone_import['zone_id'])
- self.assertEqual('NONE', zone['action'])
- self.assertEqual('ACTIVE', zone['status'])
- self.assertEqual(name, zone['name'])
+ zone = self.zones_client.show_zone(zone_import['zone_id'])[1]
+ self.assertEqual(const.NONE, zone['action'])
+ self.assertEqual(const.ACTIVE, zone['status'])
+ self.assertEqual(zone_name, zone['name'])
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_zones_transfer.py b/designate_tempest_plugin/tests/scenario/v2/test_zones_transfer.py
index 3437c33..8527e46 100644
--- a/designate_tempest_plugin/tests/scenario/v2/test_zones_transfer.py
+++ b/designate_tempest_plugin/tests/scenario/v2/test_zones_transfer.py
@@ -12,42 +12,70 @@
# 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.common import constants as const
from designate_tempest_plugin.tests import base
from designate_tempest_plugin import data_utils as dns_data_utils
+CONF = config.CONF
LOG = logging.getLogger(__name__)
class ZonesTransferTest(base.BaseDnsV2Test):
- credentials = ['primary', 'alt', 'admin']
+ credentials = ['primary', 'alt', 'admin', 'system_admin']
@classmethod
def setup_clients(cls):
super(ZonesTransferTest, cls).setup_clients()
- cls.zones_client = cls.os_primary.dns_v2.ZonesClient()
+ if CONF.enforce_scope.designate:
+ cls.admin_zones_client = cls.os_system_admin.dns_v2.ZonesClient()
+ cls.admin_accept_client = (
+ cls.os_system_admin.dns_v2.TransferAcceptClient())
+ cls.admin_tld_client = cls.os_system_admin.dns_v2.TldClient()
+ else:
+ cls.admin_zones_client = cls.os_admin.dns_v2.ZonesClient()
+ cls.admin_accept_client = (
+ cls.os_admin.dns_v2.TransferAcceptClient())
+ cls.admin_tld_client = cls.os_admin.dns_v2.TldClient()
cls.alt_zones_client = cls.os_alt.dns_v2.ZonesClient()
- cls.admin_zones_client = cls.os_admin.dns_v2.ZonesClient()
cls.request_client = cls.os_primary.dns_v2.TransferRequestClient()
cls.alt_request_client = cls.os_alt.dns_v2.TransferRequestClient()
cls.accept_client = cls.os_primary.dns_v2.TransferAcceptClient()
cls.alt_accept_client = cls.os_alt.dns_v2.TransferAcceptClient()
- cls.admin_accept_client = cls.os_admin.dns_v2.TransferAcceptClient()
+
+ @classmethod
+ def resource_setup(cls):
+ super(ZonesTransferTest, cls).resource_setup()
+
+ # Make sure we have an allowed TLD available
+ tld_name = dns_data_utils.rand_zone_name(name="ZonesTransferTest")
+ cls.tld_name = f".{tld_name}"
+ cls.class_tld = cls.admin_tld_client.create_tld(tld_name=tld_name[:-1])
+
+ @classmethod
+ def resource_cleanup(cls):
+ cls.admin_tld_client.delete_tld(cls.class_tld[1]['id'])
+ super(ZonesTransferTest, cls).resource_cleanup()
@decorators.idempotent_id('60bd80ac-c979-4686-9a03-f2f775f272ab')
def test_zone_transfer(self):
LOG.info('Create a zone as primary tenant')
- _, zone = self.zones_client.create_zone()
+ zone_name = dns_data_utils.rand_zone_name(
+ name="zone_transfer", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
self.addCleanup(self.wait_zone_delete, self.zones_client, zone['id'],
ignore_errors=lib_exc.NotFound)
LOG.info('Create a zone transfer_request for zone as primary tenant')
- _, transfer_request = \
- self.request_client.create_transfer_request_empty_body(zone['id'])
+ transfer_request = (
+ self.request_client.create_transfer_request_empty_body(
+ zone['id'])[1])
self.addCleanup(self.request_client.delete_transfer_request,
- transfer_request['id'])
+ transfer_request['id'],
+ ignore_errors=lib_exc.NotFound)
accept_data = {
"key": transfer_request['key'],
@@ -55,10 +83,16 @@
}
LOG.info('Accept the request as alt tenant')
- self.alt_accept_client.create_transfer_accept(accept_data)
+ transfer_accept = self.alt_accept_client.create_transfer_accept(
+ accept_data)[1]
+
+ LOG.info('Ensure we respond with COMPLETE status')
+ show_request = self.request_client.show_transfer_request(
+ transfer_request['id'])[1]
+ self.assertEqual(const.COMPLETE, show_request['status'])
LOG.info('Fetch the zone as alt tenant')
- _, alt_zone = self.alt_zones_client.show_zone(zone['id'])
+ alt_zone = self.alt_zones_client.show_zone(zone['id'])[1]
self.addCleanup(self.wait_zone_delete,
self.alt_zones_client,
alt_zone['id'])
@@ -73,10 +107,21 @@
lib_exc.BadRequest, 'invalid_zone_transfer_request', 400):
self.admin_accept_client.create_transfer_accept(accept_data)
+ LOG.info('Delete the transfer_request')
+ self.request_client.delete_transfer_request(transfer_request['id'])[1]
+
+ LOG.info('Validation that transfer_accept deleted'
+ ' after the transfer_request delete')
+ self.assertRaises(lib_exc.NotFound,
+ self.accept_client.show_transfer_accept,
+ transfer_accept['id'])
+
@decorators.idempotent_id('5855b772-a036-11eb-9973-74e5f9e2a801')
def test_zone_transfer_target_project(self):
LOG.info('Create a zone as "primary" tenant')
- zone = self.zones_client.create_zone()[1]
+ zone_name = dns_data_utils.rand_zone_name(
+ name="zone_transfer_target_project", suffix=self.tld_name)
+ zone = self.zones_client.create_zone(name=zone_name)[1]
LOG.info('Create transfer_request with target project set to '
'"Admin" tenant')
@@ -87,7 +132,7 @@
self.addCleanup(self.request_client.delete_transfer_request,
transfer_request['id'])
LOG.info('Ensure we respond with ACTIVE status')
- self.assertEqual('ACTIVE', transfer_request['status'])
+ self.assertEqual(const.ACTIVE, transfer_request['status'])
LOG.info('Accept the request as "alt" tenant, Expected: should fail '
'as "admin" was set as a target project.')
@@ -100,9 +145,14 @@
transfer_accept_data=accept_data)
LOG.info('Accept the request as "Admin" tenant, Expected: should work')
- self.admin_accept_client.create_transfer_accept(accept_data)
+ self.admin_accept_client.create_transfer_accept(
+ accept_data, headers={'x-auth-sudo-project-id':
+ self.os_admin.credentials.project_id},
+ extra_headers=True)
LOG.info('Fetch the zone as "Admin" tenant')
- admin_zone = self.admin_zones_client.show_zone(zone['id'])[1]
+ admin_zone = self.admin_zones_client.show_zone(
+ zone['id'], headers={'x-auth-sudo-project-id':
+ self.os_admin.credentials.project_id})[1]
self.addCleanup(self.wait_zone_delete,
self.admin_zones_client,
admin_zone['id'])
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 82eda14..ed1a773 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -31,28 +31,21 @@
Quick Start
===========
-To run all tests from this plugin, install the plugin into your environment
-and from the tempest repo, run::
+From the tempest directory, setup the tempest virtual environment for the designate tempest plugin::
- $ tox -e all-plugin -- designate
+ $ tox -e venv-tempest -- pip3 install -e <path to designate-tempest-plugin>
-If that doesn't run any tests, ensure that the designate-tempest-plugin is installed
-in the tox venv. Replace ``../designate-tempest-plugin/`` with the path to the plugin
-on your system. Then execute the above tox command again::
+To run all tests from this plugin, install designate into your environment and from the tempest repo, run::
- $ .tox/all-plugin/bin/pip install ../designate-tempest-plugin/
- $ tox -e all-plugin -- designate
-
-.. note:: This is not necessary if ``designate-tempest-plugin`` is installed to
- site-packages before the ``all-plugin`` tox venv is created.
+ $ tox -e all -- designate
To run a single test case, run with the test case name, for example::
- $ tox -e all-plugin -- designate_tempest_plugin.tests.api.v2.test_zones.ZonesAdminTest.test_get_other_tenant_zone
+ $ tox -e all -- designate_tempest_plugin.tests.api.v2.test_zones.ZonesAdminTest.test_get_other_tenant_zone
To run all tempest tests including this plugin, run::
- $ tox -e all-plugin
+ $ tox -e all
Writing new tests
=================
diff --git a/releasenotes/notes/deprecate-bug-1573141-22b980751913bc44.yaml b/releasenotes/notes/deprecate-bug-1573141-22b980751913bc44.yaml
new file mode 100644
index 0000000..6c84f22
--- /dev/null
+++ b/releasenotes/notes/deprecate-bug-1573141-22b980751913bc44.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+ - |
+ The ``[dns_feature_enabled] bug_1573141_fixed`` parameter has been
+ deprecated and will be removed in a future release.
diff --git a/requirements.txt b/requirements.txt
index 3a1f2f3..497e29e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,6 +3,7 @@
# process, which may cause wedges in the gate later.
dnspython>=1.16.0 # http://www.dnspython.org/LICENSE
-ddt>=1.0.1 # MIT
+oslo.serialization>=2.25.0 # Apache-2.0
+oslo.utils>=3.33.0 # Apache-2.0
testtools>=2.2.0 # MIT
tempest>=17.1.0 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index 3b481d0..7a5148b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-minversion = 3.1.1
+minversion = 3.18.0
envlist = pep8
skipsdist = True
ignore_basepython_conflict = True
@@ -9,13 +9,13 @@
usedevelop = True
install_command = pip install {opts} {packages}
deps =
- -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
+ -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
setenv =
VIRTUAL_ENV={envdir}
PYTHONDONTWRITEBYTECODE=1
-whitelist_externals = sh
+allowlist_externals = sh
find
rm
commands =
@@ -43,14 +43,14 @@
[testenv:releasenotes]
deps = {[testenv:docs]deps}
-whitelist_externals = rm
+allowlist_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
+allowlist_externals = bash
commands = bash -c "find {toxinidir}/devstack \
-not \( -type d -name .?\* -prune \) \
-not \( -type d -name doc -prune \) \
@@ -105,3 +105,15 @@
T112 = tempest.hacking.checks:dont_import_local_tempest_into_lib
T113 = tempest.hacking.checks:dont_use_config_in_tempest_lib
T114 = tempest.hacking.checks:use_rand_uuid_instead_of_uuid4
+ D701 = checks:mutable_default_arguments
+ D703 = checks:check_explicit_underscore_import
+ D704 = checks:no_import_graduated_oslo_libraries
+ D705 = checks:use_timeutils_utcnow
+ D706 = checks:no_translate_debug_logs
+ D707 = checks:check_no_basestring
+ D708 = checks:check_python3_xrange
+ D709 = checks:check_no_log_audit
+ D710 = checks:check_no_log_warn
+ D711 = checks:check_line_continuation_no_backslash
+paths = ./designate_tempest_plugin/hacking
+