diff --git a/mdb/forms.py b/mdb/forms.py index fb8d319..18d1ffa 100644 --- a/mdb/forms.py +++ b/mdb/forms.py @@ -10,8 +10,8 @@ class SerialNumberInput(NumberInput): """Number input for DNS SOA serials (YYYYMMDDXX). Renders wide enough to show the full 10-digit serial and adds a button - that prefills the field with today's date plus the next available XX - counter (incrementing if the field already holds today's serial). + that prefills the field with the next serial: today's first, or one more than + the current serial when that is higher. """ def __init__(self, attrs=None): @@ -35,12 +35,14 @@ if (typeof window.mdbBumpSerial === 'undefined') { var today = '' + now.getFullYear() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0'); - var current = (input.value || '').trim(); - var xx = 1; - if (current.length === 10 && current.slice(0, 8) === today) { - xx = Math.min(parseInt(current.slice(8), 10) + 1, 99); + // As mdb.utils.format_domain_serial_and_add_one: never lower than the current + // serial, so past XX = 99 it carries into the date. + var current = parseInt((input.value || '').trim(), 10); + var next = parseInt(today + '01', 10); + if (!isNaN(current) && current + 1 > next) { + next = current + 1; } - input.value = today + String(xx).padStart(2, '0'); + input.value = String(next); }; } diff --git a/mdb/signals.py b/mdb/signals.py index cba7daf..057c996 100644 --- a/mdb/signals.py +++ b/mdb/signals.py @@ -136,12 +136,12 @@ def create_pxe_key_and_write_pxe_files_when_host_changes( def update_domain_serial_when_interface_deleted(sender, instance, **kwargs): if instance.domain is not None: domain = instance.domain - domain.domain_serial += 1 + domain.domain_serial = format_domain_serial_and_add_one(domain.domain_serial) domain.save() if instance.ip4address is not None: subnet = instance.ip4address.subnet - subnet.domain_serial += 1 + subnet.domain_serial = format_domain_serial_and_add_one(subnet.domain_serial) subnet.save() diff --git a/mdb/utils.py b/mdb/utils.py index 8989fd2..50fedb1 100644 --- a/mdb/utils.py +++ b/mdb/utils.py @@ -1,34 +1,16 @@ import datetime -import re def format_domain_serial_and_add_one(serial): - today = datetime.datetime.now() - res = re.findall( - rf"^{today.year:4d}{today.month:02d}{today.day:02d}(\d\d)$", - str(serial), - re.DOTALL, - ) + """Return the serial after `serial`, in the YYYYMMDDnn form where it can be. - if len(res) == 0: - """ This probably means that the serial is malformed - or the date is wrong. We assume that if the date is wrong, - it is in the past. Just create a new serial starting from 1.""" - return f"{today.year:4d}{today.month:02d}{today.day:02d}01" - elif len(res) == 1: - """ The serial contains todays date, just update it. """ - try: - number = int(res[0]) - except ValueError: - number = 1 - if number >= 99: - """ This is bad... Just keep the number on 99. - We also send a mail to sysadmins telling them that - something is wrong...""" - - else: - number += 1 - return f"{today.year:4d}{today.month:02d}{today.day:02d}{number:02d}" - else: - """ Just return the first serial for today. """ - return f"{today.year:4d}{today.month:02d}{today.day:02d}01" + The serial must always increase, or named and dhcpd ignore the change. Today's first + serial is used while it is higher. Otherwise the serial goes up by one: past nn = 99 + it carries into the date (2026110299 -> 2026110300), and a serial already ahead of + today, from that carry or a dynamic update, keeps counting from where it is. + """ + today_first = int(datetime.date.today().strftime("%Y%m%d")) * 100 + 1 + try: + return max(int(serial) + 1, today_first) + except TypeError, ValueError: + return today_first diff --git a/tests/test_signals.py b/tests/test_signals.py index a6f9393..34d4ae5 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -1,6 +1,7 @@ import pytest +from freezegun import freeze_time -from mdb.models import DhcpConfig, Ip4Subnet, Ip6Subnet +from mdb.models import DhcpConfig, Domain, Interface, Ip4Subnet, Ip6Subnet # --------------------------------------------------------------------------- # pre_save signals: populate domain_name once, then leave it frozen @@ -65,3 +66,21 @@ def test_ip6_signal_sets_domain_name(): domain_filename="db.reverse-v6", ) assert subnet.domain_name == "0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa" + + +# --------------------------------------------------------------------------- +# pre_delete Interface: the zones it was in get a new serial +# --------------------------------------------------------------------------- + + +@freeze_time("2026-11-02") +def test_deleting_interface_raises_zone_serials(domain): + subnet = Interface.objects.get(pk=1).ip4address.subnet + Domain.objects.filter(pk=domain.pk).update(domain_serial=2026110299) + Ip4Subnet.objects.filter(pk=subnet.pk).update(domain_serial=2015010101) + + Interface.objects.get(pk=1).delete() + + # Past 99 the serial carries into the date instead of going back. + assert Domain.objects.get(pk=domain.pk).domain_serial == 2026110300 + assert Ip4Subnet.objects.get(pk=subnet.pk).domain_serial == 2026110201 diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..16a06ac --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,32 @@ +import pytest +from freezegun import freeze_time + +from mdb.utils import format_domain_serial_and_add_one + + +@freeze_time("2026-11-02") +@pytest.mark.parametrize( + ("serial", "expected"), + [ + (2026110205, 2026110206), # today: next number + (2025010107, 2026110201), # an earlier day: today's first + (1, 2026110201), # not a date at all + (2026110298, 2026110299), + (2026110299, 2026110300), # used to stay at 99, so the change never synced + (2026110300, 2026110301), # ahead of today: never goes back + (2026110505, 2026110506), + (None, 2026110201), + ("garbage", 2026110201), + ], +) +def test_next_serial(serial, expected): + assert format_domain_serial_and_add_one(serial) == expected + + +@freeze_time("2026-11-02") +def test_serial_always_increases(): + serial = 2026110290 + for _ in range(30): + new = format_domain_serial_and_add_one(serial) + assert new > serial + serial = new