summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml2
-rw-r--r--designate/backend/impl_infoblox/connector.py5
-rw-r--r--designate/backend/impl_infoblox/object_manipulator.py26
-rw-r--r--designate/common/decorators/lock.py4
-rw-r--r--designate/sqlalchemy/base.py110
-rw-r--r--designate/tests/test_sqlalchemy.py74
-rw-r--r--designate/tests/test_storage/__init__.py30
-rw-r--r--designate/tests/test_storage/test_sqlalchemy.py42
-rw-r--r--designate/tests/unit/api/test_version.py78
-rw-r--r--designate/tests/unit/backend/test_infoblox.py5
-rw-r--r--designate/tests/unit/common/__init__.py0
-rw-r--r--designate/tests/unit/common/test_zone_lock.py110
-rw-r--r--devstack/designate_plugins/backend-pdns420
-rw-r--r--devstack/designate_plugins/backend-pdns4-mysql-db.sql29
-rw-r--r--devstack/designate_plugins/backend-pdns4-pgsql-db.sql25
-rw-r--r--releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po12
-rw-r--r--setup.cfg12
17 files changed, 420 insertions, 164 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index 87c1dc8d..3c2bd1b9 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,6 +1,7 @@
- job:
name: designate-base
parent: devstack-tempest
+ nodeset: openstack-single-node-jammy
vars: &base_vars
devstack_localrc:
DESIGNATE_SERVICE_PORT_DNS: 5322
@@ -35,6 +36,7 @@
- job:
name: designate-base-ipv6-only
parent: devstack-tempest-ipv6
+ nodeset: openstack-single-node-jammy
description: |
Designate devstack-tempest base job for IPv6-only deployment
irrelevant-files: *base_irrelevant_files
diff --git a/designate/backend/impl_infoblox/connector.py b/designate/backend/impl_infoblox/connector.py
index fab9d784..aaf1456f 100644
--- a/designate/backend/impl_infoblox/connector.py
+++ b/designate/backend/impl_infoblox/connector.py
@@ -131,7 +131,10 @@ class Infoblox(object):
headers = {'Content-type': 'application/json'}
- data = jsonutils.dump_as_bytes(payload)
+ # NOTE (scottsol): This can trigger an internal error in Infoblox if
+ # jsonutils sets it to 'null' (a string with quotes). Setting to None
+ # works around this and returns a valid response from Infoblox
+ data = jsonutils.dump_as_bytes(payload) if payload else None
url = self._construct_url(objtype, query_params, extattrs)
r = self.session.get(url,
diff --git a/designate/backend/impl_infoblox/object_manipulator.py b/designate/backend/impl_infoblox/object_manipulator.py
index a2d4fdf5..dbbb431d 100644
--- a/designate/backend/impl_infoblox/object_manipulator.py
+++ b/designate/backend/impl_infoblox/object_manipulator.py
@@ -122,14 +122,16 @@ class InfobloxObjectManipulator(object):
'zone_auth',
{'fqdn': fqdn, 'view': dns_view},
{'ns_group': self.connector.ns_group,
- 'restart_if_needed': True, 'zone_format': zone_format},
+ 'zone_format': zone_format},
check_if_exists=True)
+ self._restart_if_needed()
except exc.InfobloxCannotCreateObject as e:
LOG.warning(e)
def delete_zone_auth(self, fqdn):
self._delete_infoblox_object(
'zone_auth', {'fqdn': fqdn})
+ self._restart_if_needed()
def _create_infoblox_object(self, obj_type, payload,
additional_create_kwargs=None,
@@ -205,3 +207,25 @@ class InfobloxObjectManipulator(object):
if ib_object_ref:
self.connector.delete_object(ib_object_ref)
LOG.info('Infoblox object was deleted: %s', ib_object_ref)
+
+ def _restart_if_needed(self):
+ ib_object_ref = None
+ obj_type = 'grid'
+ warn_msg = ('Infoblox %(obj_type)s will not be restarted because'
+ ' the API object reference cannot be found')
+ try:
+ ib_object_ref = self._get_infoblox_object_or_none(obj_type)
+ if not ib_object_ref:
+ LOG.warning(warn_msg, {'obj_type': obj_type})
+ except exc.InfobloxSearchError as e:
+ LOG.warning(warn_msg, {'obj_type': obj_type})
+ LOG.info(e)
+
+ if ib_object_ref:
+ payload = {
+ "restart_option": "RESTART_IF_NEEDED",
+ "mode": "GROUPED",
+ "services": ["DNS"],
+ }
+ self.connector.call_func(
+ 'restartservices', ib_object_ref, payload)
diff --git a/designate/common/decorators/lock.py b/designate/common/decorators/lock.py
index f633fa4d..97010492 100644
--- a/designate/common/decorators/lock.py
+++ b/designate/common/decorators/lock.py
@@ -52,7 +52,7 @@ def extract_zone_id(args, kwargs):
for arg in itertools.chain(args, kwargs.values()):
if not isinstance(arg, objects.DesignateObject):
continue
- if isinstance(arg, objects.Zone):
+ elif isinstance(arg, objects.Zone):
zone_id = arg.id
if zone_id:
break
@@ -68,8 +68,6 @@ def extract_zone_id(args, kwargs):
arg = args[1]
if isinstance(arg, str):
zone_id = arg
- elif isinstance(zone_id, objects.Zone):
- zone_id = arg.id
return zone_id
diff --git a/designate/sqlalchemy/base.py b/designate/sqlalchemy/base.py
index d72f36ca..d9a7eb66 100644
--- a/designate/sqlalchemy/base.py
+++ b/designate/sqlalchemy/base.py
@@ -78,7 +78,7 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
@property
def session(self):
# NOTE: This uses a thread local store, allowing each greenthread to
- # have it's own session stored correctly. Without this, each
+ # have its own session stored correctly. Without this, each
# greenthread may end up using a single global session, which
# leads to bad things happening.
@@ -101,51 +101,38 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
@staticmethod
def _apply_criterion(table, query, criterion):
- if criterion is not None:
- for name, value in criterion.items():
- column = getattr(table.c, name)
+ if criterion is None:
+ return query
+ for name, value in criterion.items():
+ column = getattr(table.c, name)
+
+ if isinstance(value, str):
# Wildcard value: '%'
- if isinstance(value, str) and '%' in value:
+ if '%' in value:
query = query.where(column.like(value))
-
- elif (isinstance(value, str) and
- value.startswith('!')):
- queryval = value[1:]
- query = query.where(column != queryval)
-
- elif (isinstance(value, str) and
- value.startswith('<=')):
- queryval = value[2:]
- query = query.where(column <= queryval)
-
- elif (isinstance(value, str) and
- value.startswith('<')):
- queryval = value[1:]
- query = query.where(column < queryval)
-
- elif (isinstance(value, str) and
- value.startswith('>=')):
- queryval = value[2:]
- query = query.where(column >= queryval)
-
- elif (isinstance(value, str) and
- value.startswith('>')):
- queryval = value[1:]
- query = query.where(column > queryval)
-
- elif (isinstance(value, str) and
- value.startswith('BETWEEN')):
- elements = [i.strip(" ") for i in
- value.split(" ", 1)[1].strip(" ").split(",")]
- query = query.where(between(
- column, elements[0], elements[1]))
-
- elif isinstance(value, list):
- query = query.where(column.in_(value))
-
+ elif value.startswith('!'):
+ query = query.where(column != value[1:])
+ elif value.startswith('<='):
+ query = query.where(column <= value[2:])
+ elif value.startswith('<'):
+ query = query.where(column < value[1:])
+ elif value.startswith('>='):
+ query = query.where(column >= value[2:])
+ elif value.startswith('>'):
+ query = query.where(column > value[1:])
+ elif value.startswith('BETWEEN'):
+ elements = [i.strip(' ') for i in
+ value.split(' ', 1)[1].strip(' ').split(',')]
+ query = query.where(
+ between(column, elements[0], elements[1])
+ )
else:
query = query.where(column == value)
+ elif isinstance(value, list):
+ query = query.where(column.in_(value))
+ else:
+ query = query.where(column == value)
return query
@@ -211,8 +198,7 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
try:
resultproxy = self.session.execute(query, [dict(values)])
except oslo_db_exception.DBDuplicateEntry:
- msg = "Duplicate %s" % obj.obj_name()
- raise exc_dup(msg)
+ raise exc_dup("Duplicate %s" % obj.obj_name())
# Refetch the row, for generated columns etc
query = select([table]).where(
@@ -247,8 +233,7 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
results = resultproxy.fetchall()
if len(results) != 1:
- msg = "Could not find %s" % cls.obj_name()
- raise exc_notfound(msg)
+ raise exc_notfound("Could not find %s" % cls.obj_name())
else:
return _set_object_from_model(cls(), results[0])
else:
@@ -300,13 +285,17 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
records_table,
recordsets_table.c.id == records_table.c.recordset_id)
- inner_q = select([recordsets_table.c.id, # 0 - RS ID
- zones_table.c.name] # 1 - ZONE NAME
- ).select_from(rzjoin).\
+ inner_q = (
+ select([recordsets_table.c.id, # 0 - RS ID
+ zones_table.c.name]). # 1 - ZONE NAME
+ select_from(rzjoin).
where(zones_table.c.deleted == '0')
+ )
- count_q = select([func.count(distinct(recordsets_table.c.id))]).\
+ count_q = (
+ select([func.count(distinct(recordsets_table.c.id))]).
select_from(rzjoin).where(zones_table.c.deleted == '0')
+ )
if index_hint:
inner_q = inner_q.with_hint(recordsets_table, index_hint,
@@ -507,7 +496,7 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
current_rrset.records.append(rrdata)
else:
- # We've already got an rrset, add the rdata
+ # We've already got a rrset, add the rdata
if record[r_map['id']] is not None:
rrdata = objects.Record()
@@ -517,8 +506,8 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
current_rrset.records.append(rrdata)
# If the last record examined was a new rrset, or there is only 1 rrset
- if len(rrsets) == 0 or \
- (len(rrsets) != 0 and rrsets[-1] != current_rrset):
+ if (len(rrsets) == 0 or
+ (len(rrsets) != 0 and rrsets[-1] != current_rrset)):
if current_rrset is not None:
rrsets.append(current_rrset)
@@ -539,9 +528,11 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
for skip_value in skip_values:
values.pop(skip_value, None)
- query = table.update()\
- .where(table.c.id == obj.id)\
- .values(**values)
+ query = (
+ table.update().
+ where(table.c.id == obj.id).
+ values(**values)
+ )
query = self._apply_tenant_criteria(context, table, query)
query = self._apply_deleted_criteria(context, table, query)
@@ -550,12 +541,10 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
try:
resultproxy = self.session.execute(query)
except oslo_db_exception.DBDuplicateEntry:
- msg = "Duplicate %s" % obj.obj_name()
- raise exc_dup(msg)
+ raise exc_dup("Duplicate %s" % obj.obj_name())
if resultproxy.rowcount != 1:
- msg = "Could not find %s" % obj.obj_name()
- raise exc_notfound(msg)
+ raise exc_notfound("Could not find %s" % obj.obj_name())
# Refetch the row, for generated columns etc
query = select([table]).where(table.c.id == obj.id)
@@ -598,8 +587,7 @@ class SQLAlchemy(object, metaclass=abc.ABCMeta):
resultproxy = self.session.execute(query)
if resultproxy.rowcount != 1:
- msg = "Could not find %s" % obj.obj_name()
- raise exc_notfound(msg)
+ raise exc_notfound("Could not find %s" % obj.obj_name())
# Refetch the row, for generated columns etc
query = select([table]).where(table.c.id == obj.id)
diff --git a/designate/tests/test_sqlalchemy.py b/designate/tests/test_sqlalchemy.py
index 8481229d..b411d579 100644
--- a/designate/tests/test_sqlalchemy.py
+++ b/designate/tests/test_sqlalchemy.py
@@ -37,79 +37,79 @@ class SQLAlchemyTestCase(TestCase):
self.query = mock.Mock()
def test_wildcard(self):
- criterion = {"a": "%foo%"}
+ criterion = {'a': '%foo%'}
- op = dummy_table.c.a.like("%foo")
+ op = dummy_table.c.a.like('%foo')
with mock.patch.object(dummy_table.c.a, 'operate') as func:
func.return_value = op
base.SQLAlchemy._apply_criterion(
dummy_table, self.query, criterion)
- func.assert_called_with(operators.like_op, "%foo%", escape=None)
+ func.assert_called_with(operators.like_op, '%foo%', escape=None)
self.query.where.assert_called_with(op)
def test_ne(self):
- criterion = {"a": "!foo"}
+ criterion = {'a': '!foo'}
- op = dummy_table.c.a != "foo"
+ op = dummy_table.c.a != 'foo'
with mock.patch.object(dummy_table.c.a, 'operate') as func:
func.return_value = op
base.SQLAlchemy._apply_criterion(
dummy_table, self.query, criterion)
- func.assert_called_with(operator.ne, "foo")
+ func.assert_called_with(operator.ne, 'foo')
self.query.where.assert_called_with(op)
def test_le(self):
- criterion = {"a": "<=foo"}
+ criterion = {'a': '<=foo'}
- op = dummy_table.c.a <= "foo"
+ op = dummy_table.c.a <= 'foo'
with mock.patch.object(dummy_table.c.a, 'operate') as func:
func.return_value = op
base.SQLAlchemy._apply_criterion(
dummy_table, self.query, criterion)
- func.assert_called_with(operator.le, "foo")
+ func.assert_called_with(operator.le, 'foo')
self.query.where.assert_called_with(op)
def test_lt(self):
- criterion = {"a": "<foo"}
+ criterion = {'a': '<foo'}
- op = dummy_table.c.a < "foo"
+ op = dummy_table.c.a < 'foo'
with mock.patch.object(dummy_table.c.a, 'operate') as func:
func.return_value = op
base.SQLAlchemy._apply_criterion(
dummy_table, self.query, criterion)
- func.assert_called_with(operator.lt, "foo")
+ func.assert_called_with(operator.lt, 'foo')
self.query.where.assert_called_with(op)
def test_ge(self):
- criterion = {"a": ">=foo"}
+ criterion = {'a': '>=foo'}
- op = dummy_table.c.a >= "foo"
+ op = dummy_table.c.a >= 'foo'
with mock.patch.object(dummy_table.c.a, 'operate') as func:
func.return_value = op
base.SQLAlchemy._apply_criterion(
dummy_table, self.query, criterion)
- func.assert_called_with(operator.ge, "foo")
+ func.assert_called_with(operator.ge, 'foo')
self.query.where.assert_called_with(op)
def test_gt(self):
- criterion = {"a": ">foo"}
+ criterion = {'a': '>foo'}
- op = dummy_table.c.a > "foo"
+ op = dummy_table.c.a > 'foo'
with mock.patch.object(dummy_table.c.a, 'operate') as func:
func.return_value = op
base.SQLAlchemy._apply_criterion(
dummy_table, self.query, criterion)
- func.assert_called_with(operator.gt, "foo")
+ func.assert_called_with(operator.gt, 'foo')
self.query.where.assert_called_with(op)
def test_between(self):
- criterion = {"a": "BETWEEN 1,3"}
+ criterion = {'a': 'BETWEEN 1,3'}
op = dummy_table.c.a.between(1, 3)
with mock.patch.object(dummy_table.c.a, 'operate') as func:
@@ -120,3 +120,39 @@ class SQLAlchemyTestCase(TestCase):
func.assert_called_with(operators.between_op, '1', '3',
symmetric=False)
self.query.where.assert_called_with(op)
+
+ def test_regular_string(self):
+ criterion = {'a': 'foo'}
+
+ op = dummy_table.c.a.like('foo')
+ with mock.patch.object(dummy_table.c.a, 'operate') as func:
+ func.return_value = op
+
+ base.SQLAlchemy._apply_criterion(
+ dummy_table, self.query, criterion)
+ func.assert_called_with(operator.eq, 'foo')
+ self.query.where.assert_called_with(op)
+
+ def test_list(self):
+ criterion = {'a': ['foo']}
+
+ op = dummy_table.c.a.between(1, 3)
+ with mock.patch.object(dummy_table.c.a, 'operate') as func:
+ func.return_value = op
+
+ base.SQLAlchemy._apply_criterion(
+ dummy_table, self.query, criterion)
+ func.assert_called_with(operators.in_op, ['foo'])
+ self.query.where.assert_called_with(op)
+
+ def test_boolean(self):
+ criterion = {'a': True}
+
+ op = dummy_table.c.a.like('foo')
+ with mock.patch.object(dummy_table.c.a, 'operate') as func:
+ func.return_value = op
+
+ base.SQLAlchemy._apply_criterion(
+ dummy_table, self.query, criterion)
+ func.assert_called_with(operator.eq, True)
+ self.query.where.assert_called_with(op)
diff --git a/designate/tests/test_storage/__init__.py b/designate/tests/test_storage/__init__.py
index be4069e8..dc93e14a 100644
--- a/designate/tests/test_storage/__init__.py
+++ b/designate/tests/test_storage/__init__.py
@@ -1544,7 +1544,7 @@ class StorageTestCase(object):
def test_create_tld(self):
values = {
'name': 'com',
- 'description': u'This is a comment.'
+ 'description': 'This is a comment.'
}
result = self.storage.create_tld(
@@ -1869,8 +1869,8 @@ class StorageTestCase(object):
def test_create_pool_with_all_relations(self):
values = {
- 'name': u'Pool',
- 'description': u'Pool description',
+ 'name': 'Pool',
+ 'description': 'Pool description',
'attributes': [{'key': 'scope', 'value': 'public'}],
'ns_records': [{'priority': 1, 'hostname': 'ns1.example.org.'}],
'nameservers': [{'host': "192.0.2.1", 'port': 53}],
@@ -2029,8 +2029,8 @@ class StorageTestCase(object):
def test_update_pool_with_all_relations(self):
values = {
- 'name': u'Pool-A',
- 'description': u'Pool-A description',
+ 'name': 'Pool-A',
+ 'description': 'Pool-A description',
'attributes': [{'key': 'scope', 'value': 'public'}],
'ns_records': [{'priority': 1, 'hostname': 'ns1.example.org.'}],
'nameservers': [{'host': "192.0.2.1", 'port': 53}],
@@ -2054,8 +2054,8 @@ class StorageTestCase(object):
# we trigger an update rather than a create.
values = {
'id': created_pool_id,
- 'name': u'Pool-B',
- 'description': u'Pool-B description',
+ 'name': 'Pool-B',
+ 'description': 'Pool-B description',
'attributes': [{'key': 'scope', 'value': 'private'}],
'ns_records': [{'priority': 1, 'hostname': 'ns2.example.org.'}],
'nameservers': [{'host': "192.0.2.5", 'port': 53}],
@@ -2534,7 +2534,7 @@ class StorageTestCase(object):
pool = self.create_pool(fixture=0)
# Create 10 PoolTargets
- created = [self.create_pool_target(pool, description=u'Target %d' % i)
+ created = [self.create_pool_target(pool, description='Target %d' % i)
for i in range(10)]
# Ensure we can page through the results.
@@ -2546,9 +2546,9 @@ class StorageTestCase(object):
# Create two pool_targets
pool_target_one = self.create_pool_target(
- pool, fixture=0, description=u'One')
+ pool, fixture=0, description='One')
pool_target_two = self.create_pool_target(
- pool, fixture=1, description=u'Two')
+ pool, fixture=1, description='Two')
# Verify pool_target_one
criterion = dict(description=pool_target_one['description'])
@@ -2588,9 +2588,9 @@ class StorageTestCase(object):
# Create two pool_targets
pool_target_one = self.create_pool_target(
- pool, fixture=0, description=u'One')
+ pool, fixture=0, description='One')
pool_target_two = self.create_pool_target(
- pool, fixture=1, description=u'Two')
+ pool, fixture=1, description='Two')
# Verify pool_target_one
criterion = dict(description=pool_target_one['description'])
@@ -2622,16 +2622,16 @@ class StorageTestCase(object):
def test_update_pool_target(self):
pool = self.create_pool(fixture=0)
- pool_target = self.create_pool_target(pool, description=u'One')
+ pool_target = self.create_pool_target(pool, description='One')
# Update the pool_target
- pool_target.description = u'Two'
+ pool_target.description = 'Two'
pool_target = self.storage.update_pool_target(
self.admin_context, pool_target)
# Verify the new values
- self.assertEqual(u'Two', pool_target.description)
+ self.assertEqual('Two', pool_target.description)
# Ensure the version column was incremented
self.assertEqual(2, pool_target.version)
diff --git a/designate/tests/test_storage/test_sqlalchemy.py b/designate/tests/test_storage/test_sqlalchemy.py
index 3133bce8..c2aeb7d1 100644
--- a/designate/tests/test_storage/test_sqlalchemy.py
+++ b/designate/tests/test_storage/test_sqlalchemy.py
@@ -30,27 +30,27 @@ class SqlalchemyStorageTest(StorageTestCase, TestCase):
def test_schema_table_names(self):
table_names = [
- u'blacklists',
- u'pool_also_notifies',
- u'pool_attributes',
- u'pool_nameservers',
- u'pool_ns_records',
- u'pool_target_masters',
- u'pool_target_options',
- u'pool_targets',
- u'pools',
- u'quotas',
- u'records',
- u'recordsets',
- u'service_statuses',
- u'tlds',
- u'tsigkeys',
- u'zone_attributes',
- u'zone_masters',
- u'zone_tasks',
- u'zone_transfer_accepts',
- u'zone_transfer_requests',
- u'zones'
+ 'blacklists',
+ 'pool_also_notifies',
+ 'pool_attributes',
+ 'pool_nameservers',
+ 'pool_ns_records',
+ 'pool_target_masters',
+ 'pool_target_options',
+ 'pool_targets',
+ 'pools',
+ 'quotas',
+ 'records',
+ 'recordsets',
+ 'service_statuses',
+ 'tlds',
+ 'tsigkeys',
+ 'zone_attributes',
+ 'zone_masters',
+ 'zone_tasks',
+ 'zone_transfer_accepts',
+ 'zone_transfer_requests',
+ 'zones'
]
inspector = self.storage.get_inspector()
diff --git a/designate/tests/unit/api/test_version.py b/designate/tests/unit/api/test_version.py
new file mode 100644
index 00000000..31903533
--- /dev/null
+++ b/designate/tests/unit/api/test_version.py
@@ -0,0 +1,78 @@
+# 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_config import cfg
+from oslo_config import fixture as cfg_fixture
+import oslotest.base
+import webtest
+
+from designate.api import versions
+from designate.common import constants
+
+
+CONF = cfg.CONF
+
+
+class TestApiVersion(oslotest.base.BaseTestCase):
+
+ def setUp(self):
+ super(TestApiVersion, self).setUp()
+ self.useFixture(cfg_fixture.Config(CONF))
+
+ def test_add_a_version(self):
+ api_url = 'http://localhost/v2'
+ results = []
+
+ versions._add_a_version(
+ results, 'v2.1', api_url, constants.EXPERIMENTAL,
+ '2022-08-10T00:00:00Z')
+
+ self.assertEqual(1, len(results))
+ self.assertEqual('v2.1', results[0]['id'])
+ self.assertEqual(constants.EXPERIMENTAL, results[0]['status'])
+ self.assertEqual('2022-08-10T00:00:00Z', results[0]['updated'])
+ self.assertEqual(2, len(results[0]['links']))
+
+ def test_get_versions(self):
+ CONF.set_override('enable_host_header', False, 'service:api')
+ CONF.set_override(
+ 'api_base_uri', 'http://127.0.0.2:9001/', 'service:api'
+ )
+
+ self.app = versions.factory({})
+ self.client = webtest.TestApp(self.app)
+
+ response = self.client.get('/')
+ self.assertEqual(200, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+
+ self.assertEqual(2, len(response.json['versions']))
+ self.assertEqual(
+ 'http://127.0.0.2:9001/v2',
+ response.json['versions'][0]['links'][0]['href']
+ )
+
+ def test_get_versions_with_enable_host_header(self):
+ CONF.set_override('enable_host_header', True, 'service:api')
+
+ self.app = versions.factory({})
+ self.client = webtest.TestApp(self.app)
+
+ response = self.client.get('/')
+ self.assertEqual(200, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+
+ self.assertEqual(2, len(response.json['versions']))
+ self.assertEqual(
+ 'http://localhost/v2',
+ response.json['versions'][0]['links'][0]['href']
+ )
diff --git a/designate/tests/unit/backend/test_infoblox.py b/designate/tests/unit/backend/test_infoblox.py
index 81c8351b..731757c4 100644
--- a/designate/tests/unit/backend/test_infoblox.py
+++ b/designate/tests/unit/backend/test_infoblox.py
@@ -88,6 +88,11 @@ class InfobloxBackendTestCase(oslotest.base.BaseTestCase):
json={},
)
+ req_mock.get(
+ '%s/v2.0/grid' % self.base_address,
+ json={},
+ )
+
self.backend.create_zone(self.context, self.zone)
self.backend.delete_zone(self.context, self.zone)
diff --git a/designate/tests/unit/common/__init__.py b/designate/tests/unit/common/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/designate/tests/unit/common/__init__.py
diff --git a/designate/tests/unit/common/test_zone_lock.py b/designate/tests/unit/common/test_zone_lock.py
new file mode 100644
index 00000000..e2d7f4b1
--- /dev/null
+++ b/designate/tests/unit/common/test_zone_lock.py
@@ -0,0 +1,110 @@
+# 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 unittest import mock
+
+import oslotest.base
+
+from designate.common.decorators import lock
+from designate import objects
+
+
+class TestExtractZoneId(oslotest.base.BaseTestCase):
+ def setUp(self):
+ super(TestExtractZoneId, self).setUp()
+
+ def test_extract_zone_id_empty(self):
+ self.assertIsNone(lock.extract_zone_id([], {}))
+
+ def test_extract_zone_id_no_valid_objects(self):
+ self.assertIsNone(
+ lock.extract_zone_id([], {
+ 'ptr': objects.PTRList(), 'a': objects.AList()})
+ )
+
+ def test_extract_zone_id_kwargs(self):
+ self.assertEqual(
+ 'test',
+ lock.extract_zone_id([], {'zone_id': 'test'})
+ )
+ self.assertEqual(
+ 'test',
+ lock.extract_zone_id([], {'zone': mock.Mock(id='test')})
+ )
+ self.assertEqual(
+ 'test',
+ lock.extract_zone_id([], {'recordset': mock.Mock(zone_id='test')})
+ )
+ self.assertEqual(
+ 'test',
+ lock.extract_zone_id([], {'record': mock.Mock(zone_id='test')})
+ )
+
+ def test_extract_zone_id_from_zone(self):
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id(['a', 'b', 'c'], {'x': objects.Zone(id=123)})
+ )
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id([objects.Zone(id=123)], {})
+ )
+
+ def test_extract_zone_id_from_recordset(self):
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id([], {'x': objects.RecordSet(zone_id=123)})
+ )
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id([objects.RecordSet(zone_id=123)], {})
+ )
+
+ def test_extract_zone_id_from_record(self):
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id([], {'x': objects.Record(zone_id=123)})
+ )
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id([objects.Record(zone_id=123)], {})
+ )
+
+ def test_extract_zone_id_from_zone_transfer_request(self):
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id(
+ [], {'x': objects.ZoneTransferRequest(zone_id=123)})
+ )
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id(
+ [objects.ZoneTransferRequest(zone_id=123)], {})
+ )
+
+ def test_extract_zone_id_from_zone_transfer_accept(self):
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id(
+ [], {'x': objects.ZoneTransferAccept(zone_id=123)})
+ )
+ self.assertEqual(
+ '123',
+ lock.extract_zone_id([objects.ZoneTransferAccept(zone_id=123)], {})
+ )
+
+ def test_extract_zone_id_from_second_argument(self):
+ self.assertEqual('456', lock.extract_zone_id(['123', '456'], {}))
+
+ def test_extract_zone_id_when_second_argument_is_a_zone(self):
+ self.assertEqual(
+ '456', lock.extract_zone_id(['123', objects.Zone(id=456)], {})
+ )
diff --git a/devstack/designate_plugins/backend-pdns4 b/devstack/designate_plugins/backend-pdns4
index 406f57be..a6c8962d 100644
--- a/devstack/designate_plugins/backend-pdns4
+++ b/devstack/designate_plugins/backend-pdns4
@@ -46,7 +46,10 @@ function install_designate_backend {
fi
install_package $PDNS
- sudo rm -rf $POWERDNS_CFG_DIR/pdns.d
+
+ # We need to wait for the configuration and database to be in place
+ # before we can start up pdns4. Otherwise it will just crash in a loop.
+ stop_designate_backend
}
# configure_designate_backend - make configuration changes, including those to other services
@@ -88,12 +91,12 @@ EOF
setgid=pdns
setuid=pdns
config-dir=$POWERDNS_CFG_DIR
-socket-dir=/var/run
guardian=yes
daemon=yes
disable-axfr=no
-local-address=$HOST_IP
-local-ipv6=$HOST_IPV6
+# local-address should always be set, we temporarily
+# commented it out due to compatibility issues with pdns 4.3.
+# local-address=$HOST_IP $HOST_IPV6
local-port=$DESIGNATE_SERVICE_PORT_DNS
master=no
slave=yes
@@ -134,17 +137,10 @@ EOF
else
die $LINENO "PDNS4 backend only supports MySQL / pgSQL"
fi
- restart_service pdns
}
# init_designate_backend - initialize databases, etc.
function init_designate_backend {
- # Stop pdns so that the migration succeeds, if not you get a error
- # that the schema is still in use.
- if is_service_enabled postgresql; then
- stop_designate_backend
- fi
-
# (Re)create designate_pdns database
recreate_database designate_pdns utf8
if is_service_enabled mysql; then
@@ -154,6 +150,8 @@ function init_designate_backend {
else
die $LINENO "PDNS4 backend only supports MySQL"
fi
+
+ restart_service pdns
}
# create_designate_pool_configuration_backend - Perform post-pool config tasks
diff --git a/devstack/designate_plugins/backend-pdns4-mysql-db.sql b/devstack/designate_plugins/backend-pdns4-mysql-db.sql
index 6fc86472..46db3bdd 100644
--- a/devstack/designate_plugins/backend-pdns4-mysql-db.sql
+++ b/devstack/designate_plugins/backend-pdns4-mysql-db.sql
@@ -1,27 +1,31 @@
+-- Based on https://docs.powerdns.com/authoritative/backends/generic-mysql.html#default-schema
+
CREATE TABLE domains (
id INT AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
master VARCHAR(128) DEFAULT NULL,
last_check INT DEFAULT NULL,
- type VARCHAR(6) NOT NULL,
- notified_serial INT DEFAULT NULL,
- account VARCHAR(40) DEFAULT NULL,
+ type VARCHAR(8) NOT NULL,
+ notified_serial INT UNSIGNED DEFAULT NULL,
+ account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
+ options TEXT DEFAULT NULL,
+ catalog VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (id)
) Engine=InnoDB;
CREATE UNIQUE INDEX name_index ON domains(name);
+CREATE INDEX catalog_idx ON domains(catalog);
CREATE TABLE records (
- id INT AUTO_INCREMENT,
+ id BIGINT AUTO_INCREMENT,
domain_id INT DEFAULT NULL,
name VARCHAR(255) DEFAULT NULL,
type VARCHAR(10) DEFAULT NULL,
- -- Changed to "TEXT", as VARCHAR(65000) is too big for most MySQL installs
+ -- Changed to "TEXT", as VARCHAR(64000) is too big for most MySQL installs
content TEXT DEFAULT NULL,
ttl INT DEFAULT NULL,
prio INT DEFAULT NULL,
- change_date INT DEFAULT NULL,
disabled TINYINT(1) DEFAULT 0,
ordername VARCHAR(255) BINARY DEFAULT NULL,
auth TINYINT(1) DEFAULT 1,
@@ -30,13 +34,13 @@ CREATE TABLE records (
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
-CREATE INDEX recordorder ON records (domain_id, ordername);
+CREATE INDEX ordername ON records (ordername);
CREATE TABLE supermasters (
ip VARCHAR(64) NOT NULL,
nameserver VARCHAR(255) NOT NULL,
- account VARCHAR(40) NOT NULL,
+ account VARCHAR(40) CHARACTER SET 'utf8' NOT NULL,
PRIMARY KEY (ip, nameserver)
) Engine=InnoDB;
@@ -47,9 +51,9 @@ CREATE TABLE comments (
name VARCHAR(255) NOT NULL,
type VARCHAR(10) NOT NULL,
modified_at INT NOT NULL,
- account VARCHAR(40) NOT NULL,
- -- Changed to "TEXT", as VARCHAR(65000) is too big for most MySQL installs
- comment TEXT NOT NULL,
+ account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
+ -- Changed to "TEXT", as VARCHAR(64000) is too big for most MySQL installs
+ comment TEXT CHARACTER SET 'utf8' NOT NULL,
PRIMARY KEY (id)
) Engine=InnoDB;
@@ -74,6 +78,7 @@ CREATE TABLE cryptokeys (
domain_id INT NOT NULL,
flags INT NOT NULL,
active BOOL,
+ published BOOL DEFAULT 1,
content TEXT,
PRIMARY KEY(id)
) Engine=InnoDB;
@@ -89,4 +94,4 @@ CREATE TABLE tsigkeys (
PRIMARY KEY (id)
) Engine=InnoDB;
-CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);
+CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm); \ No newline at end of file
diff --git a/devstack/designate_plugins/backend-pdns4-pgsql-db.sql b/devstack/designate_plugins/backend-pdns4-pgsql-db.sql
index 6856b3e5..694b2e82 100644
--- a/devstack/designate_plugins/backend-pdns4-pgsql-db.sql
+++ b/devstack/designate_plugins/backend-pdns4-pgsql-db.sql
@@ -1,31 +1,37 @@
+-- Based on https://docs.powerdns.com/authoritative/backends/generic-postgresql.html#default-schema
+
CREATE TABLE domains (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
master VARCHAR(128) DEFAULT NULL,
last_check INT DEFAULT NULL,
- type VARCHAR(6) NOT NULL,
- notified_serial INT DEFAULT NULL,
- account VARCHAR(40) DEFAULT NULL
+ type TEXT NOT NULL,
+ notified_serial BIGINT DEFAULT NULL,
+ account VARCHAR(40) DEFAULT NULL,
+ options TEXT DEFAULT NULL,
+ catalog TEXT DEFAULT NULL,
+ CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);
CREATE UNIQUE INDEX name_index ON domains(name);
+CREATE INDEX catalog_idx ON domains(catalog);
CREATE TABLE records (
- id SERIAL PRIMARY KEY,
+ id BIGSERIAL PRIMARY KEY,
domain_id INT DEFAULT NULL,
name VARCHAR(255) DEFAULT NULL,
type VARCHAR(10) DEFAULT NULL,
content VARCHAR(65535) DEFAULT NULL,
ttl INT DEFAULT NULL,
prio INT DEFAULT NULL,
- change_date INT DEFAULT NULL,
disabled BOOL DEFAULT 'f',
ordername VARCHAR(255),
auth BOOL DEFAULT 't',
CONSTRAINT domain_exists
FOREIGN KEY(domain_id) REFERENCES domains(id)
- ON DELETE CASCADE
+ ON DELETE CASCADE,
+ CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);
CREATE INDEX rec_name_index ON records(name);
@@ -52,7 +58,8 @@ CREATE TABLE comments (
comment VARCHAR(65535) NOT NULL,
CONSTRAINT domain_exists
FOREIGN KEY(domain_id) REFERENCES domains(id)
- ON DELETE CASCADE
+ ON DELETE CASCADE,
+ CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);
CREATE INDEX comments_domain_id_idx ON comments (domain_id);
@@ -75,6 +82,7 @@ CREATE TABLE cryptokeys (
domain_id INT REFERENCES domains(id) ON DELETE CASCADE,
flags INT NOT NULL,
active BOOL,
+ published BOOL DEFAULT TRUE,
content TEXT
);
@@ -85,7 +93,8 @@ CREATE TABLE tsigkeys (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
algorithm VARCHAR(50),
- secret VARCHAR(255)
+ secret VARCHAR(255),
+ CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
);
CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);
diff --git a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po
index 7ff248ae..22363700 100644
--- a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po
+++ b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po
@@ -8,11 +8,11 @@ msgid ""
msgstr ""
"Project-Id-Version: Designate Release Notes\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-10-15 00:52+0000\n"
+"POT-Creation-Date: 2022-10-25 21:48+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2022-10-16 10:42+0000\n"
+"PO-Revision-Date: 2022-11-04 10:30+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n"
"Language: en_GB\n"
@@ -46,8 +46,8 @@ msgstr "12.0.0"
msgid "12.1.0"
msgstr "12.1.0"
-msgid "12.1.0-4"
-msgstr "12.1.0-4"
+msgid "12.1.0-5"
+msgstr "12.1.0-5"
msgid "13.0.0"
msgstr "13.0.0"
@@ -70,8 +70,8 @@ msgstr "14.0.1-5"
msgid "15.0.0"
msgstr "15.0.0"
-msgid "15.0.0.0rc1-11"
-msgstr "15.0.0.0rc1-11"
+msgid "15.0.0.0rc1-14"
+msgstr "15.0.0.0rc1-14"
msgid "2.0.0"
msgstr "2.0.0"
diff --git a/setup.cfg b/setup.cfg
index bad15c98..4d223650 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,13 +1,13 @@
[metadata]
name = designate
-summary = DNS as a Service
-description_file =
- README.rst
+description = DNS as a Service
+long_description = file: README.rst
+
author = OpenStack
author_email = openstack-discuss@lists.openstack.org
-home_page = https://docs.openstack.org/designate/latest/
+url = https://docs.openstack.org/designate/latest/
python_requires = >=3.8
-classifier =
+classifiers =
Environment :: OpenStack
Environment :: No Input/Output (Daemon)
Intended Audience :: Information Technology
@@ -19,7 +19,7 @@ classifier =
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
- Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
Topic :: Internet :: Name Service (DNS)
[files]