From f0aeb51d9b6fdf472de6324a2b91e84a99c05b89 Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Mon, 5 Oct 2026 17:33:00 -0400 Subject: [PATCH] ad: add zone creation check to the AD DNS class There is an undocumented hard coded 60 second limit that causes a race condition with freshly created zones. Verifies zone readiness via Resolve-DnsName (a real DNS protocol query) instead of Get-DnsServerResourceRecord (a management-API check), since the management API can report the zone as registered before the DNS service is actually answering authoritatively for it, which is what nsupdate relies on during its own SOA discovery. AI: Claude Sonnet 4.6 Co-authored-by: Cursor --- sssd_test_framework/roles/ad.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/sssd_test_framework/roles/ad.py b/sssd_test_framework/roles/ad.py index 4d325f57..4eb51bf6 100644 --- a/sssd_test_framework/roles/ad.py +++ b/sssd_test_framework/roles/ad.py @@ -5,6 +5,7 @@ import os import re import textwrap +import time import uuid from datetime import datetime from typing import Any, TypeAlias, cast @@ -2463,10 +2464,30 @@ def create(self) -> ADDNSZone: """ Create new zone. + Zone creation with ``-ReplicationScope Forest`` is AD-integrated and + asynchronous: the cmdlet returns as soon as the zone is registered. + + NSUpdate SOA query can walk past the brand-new zone to a parent zone + (e.g. in-addr.arpa resolving externally), causing GSS-TSIG to fail against the + wrong server even though the zone "exists" per the management API. :return: Self. :rtype: ADDNSZone """ - self.host.conn.run(f"Add-DnsServerPrimaryZone -Name {self.zone_name} -ReplicationScope Forest -Passthru") + self.host.conn.run( + f"Add-DnsServerPrimaryZone -Name {self.zone_name} -ReplicationScope Forest -DynamicUpdate Secure -Passthru" + ) + + for _ in range(30): + result = self.host.conn.run( + f'Resolve-DnsName -Name "{self.zone_name}" -Type SOA -Server 127.0.0.1 -ErrorAction SilentlyContinue', + raise_on_error=False, + ) + if result.rc == 0 and self.zone_name.lower() in result.stdout.lower(): + break + time.sleep(1) + else: + raise TimeoutError(f"Zone '{self.zone_name}' did not become resolvable within 30 seconds!") + return self def delete(self) -> None: