diff options
-rw-r--r-- | .zuul.yaml | 2 | ||||
-rw-r--r-- | designate/backend/impl_infoblox/connector.py | 5 | ||||
-rw-r--r-- | designate/backend/impl_infoblox/object_manipulator.py | 26 | ||||
-rw-r--r-- | designate/common/decorators/lock.py | 4 | ||||
-rw-r--r-- | designate/sqlalchemy/base.py | 110 | ||||
-rw-r--r-- | designate/tests/test_sqlalchemy.py | 74 | ||||
-rw-r--r-- | designate/tests/test_storage/__init__.py | 30 | ||||
-rw-r--r-- | designate/tests/test_storage/test_sqlalchemy.py | 42 | ||||
-rw-r--r-- | designate/tests/unit/api/test_version.py | 78 | ||||
-rw-r--r-- | designate/tests/unit/backend/test_infoblox.py | 5 | ||||
-rw-r--r-- | designate/tests/unit/common/__init__.py | 0 | ||||
-rw-r--r-- | designate/tests/unit/common/test_zone_lock.py | 110 | ||||
-rw-r--r-- | devstack/designate_plugins/backend-pdns4 | 20 | ||||
-rw-r--r-- | devstack/designate_plugins/backend-pdns4-mysql-db.sql | 29 | ||||
-rw-r--r-- | devstack/designate_plugins/backend-pdns4-pgsql-db.sql | 25 | ||||
-rw-r--r-- | releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po | 12 | ||||
-rw-r--r-- | setup.cfg | 12 |
17 files changed, 420 insertions, 164 deletions
@@ -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" @@ -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] |