Support using non unix timestamp serial.

RFC1982 allows to have different formats including:
    - Simple increment of any number
    - Unixtime
    - Date with counter format YYYYMMDDNN
This changes allow to use any supported serial format and fix some pep8 errors for related code.
Serial will be increased to current timestamp if current timestamp between zone created_at and current timestamp
Serial will be increased to +1 if current timestamp less or bigger then zone created_at or bigger then current timestamp

Depends-On: https://review.opendev.org/c/openstack/designate-tempest-plugin/+/915069
Closes-bug: #2053022
Change-Id: I52f7834c423ed3fdbfe2441780f917db658a2bcb
Signed-off-by: Mikhail Samoylov <mikhailsamoiloff@gmail.com>
This commit is contained in:
Mikhail Samoylov 2024-03-29 13:15:37 +04:00 committed by mikhails
parent 097ffc6df1
commit 2a531a9b2f
9 changed files with 162 additions and 21 deletions

View File

@ -40,6 +40,7 @@ Request
- name: zone_name
- email: zone_email
- ttl: zone_ttl
- serial: zone_serial
- description: zone_description
- masters: zone_masters
- type: zone_type
@ -321,6 +322,7 @@ Request
- x-auth-sudo-project-id: x-auth-sudo-project-id
- zone_id: path_zone_id
- email: zone_email_update
- serial: zone_serial
- ttl: zone_ttl
- description: zone_description

View File

@ -168,7 +168,8 @@ class ZonesController(rest.RestController):
# Update and persist the resource
increment_serial = zone.type == 'PRIMARY'
increment_serial = (zone.type == 'PRIMARY' and
'serial' not in zone.obj_get_changes())
zone = self.central_api.update_zone(
context, zone, increment_serial=increment_serial)

View File

@ -721,11 +721,24 @@ class Service(service.RPCService):
pool = self.storage.get_pool(elevated_context, pool_id)
return pool.ns_records
def _generate_record_serial(self, zone, increment_serial):
if not increment_serial:
return zone.serial
else:
return zone.serial + 1
@rpc.expected_exceptions()
@transaction
@lock.synchronized_zone()
def increment_zone_serial(self, context, zone):
zone.serial = self.storage.increment_serial(context, zone.id)
if zone.created_at.timestamp() < zone.serial < timeutils.utcnow_ts():
zone.serial = self.storage.increment_serial(
context, zone.id, timeutils.utcnow_ts()
)
else:
zone.serial = self.storage.increment_serial(
context, zone.id, zone.serial + 1
)
self._update_soa(context, zone)
return zone.serial
@ -1512,10 +1525,9 @@ class Service(service.RPCService):
for record in recordset.records:
record.action = 'CREATE'
record.status = 'PENDING'
if not increment_serial:
record.serial = zone.serial
else:
record.serial = timeutils.utcnow_ts()
record.serial = self._generate_record_serial(
zone, increment_serial
)
new_recordset = self.storage.create_recordset(context, zone.id,
recordset)
@ -1739,10 +1751,9 @@ class Service(service.RPCService):
continue
record.action = 'UPDATE'
record.status = 'PENDING'
if not increment_serial:
record.serial = zone.serial
else:
record.serial = timeutils.utcnow_ts()
record.serial = self._generate_record_serial(
zone, increment_serial
)
# Ensure the tenant has enough zone record quotas to
# create new records
@ -1827,10 +1838,9 @@ class Service(service.RPCService):
for record in recordset.records:
record.action = 'DELETE'
record.status = 'PENDING'
if not increment_serial:
record.serial = zone.serial
else:
record.serial = timeutils.utcnow_ts()
record.serial = self._generate_record_serial(
zone, increment_serial
)
# Update the recordset's action/status and then delete it
self.storage.update_recordset(context, recordset)

View File

@ -38,7 +38,9 @@ class ZoneAPIv2Adapter(base.APIv2Adapter):
"ttl": {
'read_only': False
},
"serial": {},
"serial": {
'read_only': False
},
"shared": {},
"status": {},
"action": {},

View File

@ -97,6 +97,22 @@ class Zone(base.DesignateObject, base.DictObjectMixin,
errors = ValidationErrorList()
if self.type == 'PRIMARY':
if 'serial' in self.obj_what_changed():
if self.obj_get_original_value('serial') > self.serial:
e = ValidationError()
e.path = ['type']
e.validator = 'minimum'
e.validator_value = ['serial']
e.message = (
"serial %d is less than previously set serial of %d" %
(
self.serial,
self.obj_get_original_value('serial')
)
)
errors.append(e)
else:
self.serial -= 1 # increment zone has to be called later
if self.obj_attr_is_set('masters') and len(self.masters) != 0:
e = ValidationError()
e.path = ['type']
@ -123,7 +139,7 @@ class Zone(base.DesignateObject, base.DictObjectMixin,
e.message = "'masters' is a required property"
errors.append(e)
for i in ['email', 'ttl']:
for i in ['email', 'ttl', 'serial']:
if i in self.obj_what_changed():
e = ValidationError()
e.path = ['type']

View File

@ -15,7 +15,6 @@
# under the License.
from oslo_log import log as logging
from oslo_utils.secretutils import md5
from oslo_utils import timeutils
from sqlalchemy import case, select, distinct, func
from sqlalchemy.sql.expression import or_, literal_column
@ -621,10 +620,9 @@ class SQLAlchemyStorage(base.SQLAlchemy):
return updated_zone
def increment_serial(self, context, zone_id):
def increment_serial(self, context, zone_id, new_serial):
"""Increment the zone's serial number.
"""
new_serial = timeutils.utcnow_ts()
query = tables.zones.update().where(
tables.zones.c.id == zone_id).values(
{'serial': new_serial, 'increment_serial': False}

View File

@ -422,6 +422,32 @@ class ApiV2ZonesTest(v2.ApiV2TestCase):
self.assertEqual('prefix-%s' % zone['email'],
response.json['email'])
def test_update_zone_serial(self):
# Create a zone
zone = self.create_zone()
new_serial = zone['serial'] + 10000
# Prepare an update body
body = {'serial': new_serial}
response = self.client.patch_json('/zones/%s' % zone['id'], body,
status=202)
# Check the headers are what we expect
self.assertEqual(202, response.status_int)
self.assertEqual('application/json', response.content_type)
# Check the body structure is what we expect
self.assertIn('links', response.json)
self.assertIn('self', response.json['links'])
self.assertIn('status', response.json)
# Check the values returned are what we expect
self.assertIn('id', response.json)
self.assertIsNotNone(response.json['updated_at'])
self.assertEqual(new_serial - 1, response.json['serial'])
def test_update_zone_invalid_id(self):
self._assert_invalid_uuid(self.client.patch_json, '/zones/%s',
headers={'X-Test-Role': 'member'})

View File

@ -1037,6 +1037,23 @@ class CentralServiceTest(designate.tests.functional.TestCase):
self.assertFalse(zone.increment_serial)
self.assertEqual('info@example.net', zone.email)
def test_update_zone_serial(self):
# Create a zone
zone = self.create_zone(email='info@example.org')
new_serial = zone.serial + 10000
zone.serial = new_serial
# Perform the update
self.central_service.update_zone(
self.admin_context, zone, increment_serial=False)
# Fetch the zone again
zone = self.central_service.get_zone(self.admin_context, zone.id)
# Ensure the zone was updated correctly
self.assertEqual(new_serial, zone.serial)
def test_update_zone_name_fail(self):
# Create a zone
zone = self.create_zone(name='example.org.')
@ -4542,7 +4559,7 @@ class CentralServiceTest(designate.tests.functional.TestCase):
)
# Ensure that serial is now correct.
self.assertEqual(zone_serial + 5, updated_zone.serial)
self.assertEqual(zone_serial + 1, updated_zone.serial)
self.assertFalse(updated_zone.increment_serial)
# But the zone is still in pending status as we haven't notified
@ -4570,7 +4587,7 @@ class CentralServiceTest(designate.tests.functional.TestCase):
# Validate that the status is now ACTIVE.
self.assertEqual('ACTIVE', updated_zone.status)
self.assertEqual(zone_serial + 5, updated_zone.serial)
self.assertEqual(zone_serial + 1, updated_zone.serial)
for recordset in recordsets:
self.assertEqual('ACTIVE', recordset.status)
for record in recordset.records:
@ -4663,3 +4680,71 @@ class CentralServiceTest(designate.tests.functional.TestCase):
exceptions.Forbidden,
self.central_service._enforce_catalog_zone_policy,
self.get_context(), catalog_zone)
def test_create_zone_serial_yyyymmddss(self):
serial = 2024080201
# Create a zone
zone = self.create_zone(email='info@example.org', serial=serial)
self.assertEqual(serial, self.central_service.get_zone(
self.admin_context, zone['id']).serial)
def test_create_zone_serial_unixtime(self):
serial = 1369550494
# Create a zone
zone = self.create_zone(email='info@example.org', serial=serial)
self.assertEqual(serial, self.central_service.get_zone(
self.admin_context, zone['id']).serial)
def test_create_zone_serial_number(self):
serial = 1234567
# Create a zone
zone = self.create_zone(email='info@example.org', serial=serial)
self.assertEqual(serial, self.central_service.get_zone(
self.admin_context, zone['id']).serial)
@mock.patch.object(notifier.Notifier, "info")
def test_update_zone_serial_to_yyyymmddss(self, mock_notifier):
serial = 2024080201
# Create a zone
zone = self.create_zone(email='info@example.org', serial=1)
# Update the object
zone.serial = serial
# Reset the mock to avoid the calls from the create_zone() call
mock_notifier.reset_mock()
# Perform the update
self.central_service.update_zone(self.admin_context, zone)
self.assertEqual(serial, self.central_service.get_zone(
self.admin_context, zone['id']).serial)
@mock.patch.object(notifier.Notifier, "info")
def test_update_zone_serial_to_unixtime(self, mock_notifier):
serial = 1708636627
# Create a zone
zone = self.create_zone(email='info@example.org', serial=1)
# Update the object
zone.serial = serial
# Reset the mock to avoid the calls from the create_zone() call
mock_notifier.reset_mock()
# Perform the update
self.central_service.update_zone(self.admin_context, zone)
self.assertEqual(serial, self.central_service.get_zone(
self.admin_context, zone['id']).serial)
@mock.patch.object(notifier.Notifier, "info")
def test_update_zone_serial_to_number(self, mock_notifier):
serial = 1234567
# Create a zone
zone = self.create_zone(email='info@example.org', serial=1)
# Update the object
zone.serial = serial
# Reset the mock to avoid the calls from the create_zone() call
mock_notifier.reset_mock()
# Perform the update
self.central_service.update_zone(self.admin_context, zone)
self.assertEqual(serial, self.central_service.get_zone(
self.admin_context, zone['id']).serial)

View File

@ -146,6 +146,7 @@ function configure_designate_tempest() {
iniset $TEMPEST_CONFIG dns_feature_enabled api_admin $DESIGNATE_ENABLE_API_ADMIN
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_root_recordsets True
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_quotas True
iniset $TEMPEST_CONFIG dns_feature_enabled zone_serial_update_disabled False
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_quotas_verify_project True
iniset $TEMPEST_CONFIG dns_feature_enabled bug_1573141_fixed True
iniset $TEMPEST_CONFIG dns_feature_enabled bug_1932026_fixed True