From c527f10d7d5ab0b37d8ca93b28621bcb270cab19 Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Thu, 16 Apr 2026 15:26:04 -0400 Subject: [PATCH 1/7] fixed ip_to_ptr method and added ip_is_valid method --- sssd_test_framework/misc/__init__.py | 53 ++++++++++++++++++++++------ tests/test_misc.py | 12 ++++--- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/sssd_test_framework/misc/__init__.py b/sssd_test_framework/misc/__init__.py index 4919b6cc..ed539072 100644 --- a/sssd_test_framework/misc/__init__.py +++ b/sssd_test_framework/misc/__init__.py @@ -198,34 +198,67 @@ def seconds_to_timespan(seconds: int) -> str: return f"{d:02d}:{h:02d}:{m:02d}:{s:02d}:00" -def ip_to_ptr(ip_address: str) -> str: +def ip_to_ptr(ip_address: str, prefixlen: int | None = None) -> str: """ Get the reverse pointer from given address. :param ip_address: Address. :type ip_address: str + :param prefixlen: Prefix length, optional + :type prefixlen: int | None = None :return: Reverse pointer. :rtype: str """ ip = ipaddress.ip_address(ip_address) + network = ipaddress.ip_network(ip) + + if prefixlen is not None: + network = network.supernet(new_prefix=prefixlen) + elif ip.version == 4: + prefixlen = 24 + else: + prefixlen = 64 + + if prefixlen > network.max_prefixlen: + raise ValueError(f"Prefix {prefixlen} too large for {ip.version}!") + + subnet = network.supernet(new_prefix=prefixlen) + if ip.version == 4: - octets = ip.packed - ptr = f"{octets[2]}.{octets[1]}.{octets[0]}.in-addr.arpa." - elif ip.version == 6: - hex_parts = ip.exploded.replace(":", "").lower() - ptr = f"{hex_parts[::-1]}.ip6.arpa." + octets = subnet.network_address.packed + parts = [str(octets[i]) for i in range((prefixlen // 8) - 1, -1, -1)] + return ".".join(parts) + ".in-addr.arpa" else: - raise ValueError("Unsupported IP version") - return ptr + hex_str = subnet.network_address.exploded.replace(":", "").lower() + nibbles = list(hex_str) + relevant_nibbles = nibbles[: prefixlen // 4] + reversed_nibbles = ".".join(relevant_nibbles[::-1]) + return f"{reversed_nibbles}.ip6.arpa" + + +def ip_is_valid(ip: str) -> bool: + """ + Check ip is valid. + + :param ip: IP address. + :type ip: str + :return: True, false if str is not an ip. + :rtype: bool + """ + try: + ipaddress.ip_address(ip) + return True + except ValueError: + return False def ip_version(ip_address: str) -> int | None: """ Parse str and return the IP version. - ::param ip_address: IP address. + :param ip_address: IP address. :type ip_address: str - :return: IP version or None if not found. + :return: IP version or None if not found. :rtype: int | None """ try: diff --git a/tests/test_misc.py b/tests/test_misc.py index 867c5262..ecbb9a7f 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -288,14 +288,16 @@ def test_seconds_to_timespan(seconds: tuple[int, str]): @pytest.mark.parametrize( - "value, expected", + "value, expected, prefixlen", [ - ("192.168.1.0", "1.168.192.in-addr.arpa."), - ("2001:db8::1", "1000000000000000000000008bd01002.ip6.arpa."), + ("192.168.1.0", "1.168.192.in-addr.arpa", None), + ("10.20.30.40", "20.10.in-addr.arpa", 16), + ("fe80::48b2:6cff:fefc:17ff", "0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa", None), + ("2001:db8:abcd:1234::1", "4.3.2.1.d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa", None), ], ) -def test_ip_to_ptr(value, expected): - assert ip_to_ptr(value) == expected +def test_ip_to_ptr(value, expected, prefixlen): + assert ip_to_ptr(value, prefixlen) == expected @pytest.mark.parametrize( From f4a8f5b28b8f91aef9522fe9d91d29a2e2a32b8e Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Thu, 16 Apr 2026 15:28:19 -0400 Subject: [PATCH 2/7] improving dig method --- sssd_test_framework/utils/network.py | 80 ++++++++++++++-------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/sssd_test_framework/utils/network.py b/sssd_test_framework/utils/network.py index 671a7fc8..8cc784d7 100644 --- a/sssd_test_framework/utils/network.py +++ b/sssd_test_framework/utils/network.py @@ -9,6 +9,7 @@ from pytest_mh.conn import ProcessResult from pytest_mh.utils.fs import LinuxFileSystem +from ..misc import ip_is_valid from ..misc.ssh import SSHKillableProcess __all__ = ["NetworkUtils", "IPUtils"] @@ -77,76 +78,72 @@ def tshark(self, args: list[Any] | None = None) -> ProcessResult: return self.host.conn.exec(["tshark", *args]) - def dig(self, address: str, server: str = "", reverse: bool = False) -> dict[str, Any] | None: + def dig(self, address: str, server: str | None = None) -> list[dict] | None: """ Execute and parse dig command. - This returns a dictionary with the following keys: + This returns a list of dicts with the following keys: {name, type, ttl, data, all_data} - If the result contains more than one record, this will return the first result only. - Making it easier to access the values. The full result is available by getting the - ``all_data`` key. - .. code-block:: python :caption: Example usage # Assert the record exists - assert not client.net.dig(f"client.{provider.domain}") + assert client.net.dig(hostname, provider.server) + assert any(r["data"] == ip for r in client.net.dig(hostname, provider.server)) - # Get the TTL - result = client.net.dig(f"client.{provider.domain}") - ttl = result.get("ttl") + # Assert the reverse record exists + assert client.net.dig(ip, provider.server) + assert any(r["data"] == hostname for r in client.net.dig(ip, provider.server)) - :param address: Hostname or IP address with reverse set. + :param address: Hostname or ip. :type address: str :param server: DNS server, optional, defaults to "" :type server: str = "" - :param reverse: Do a reverse lookup, optional, defaults to False - :type reverse: bool = False - :return: Dict . - :rtype: tuple[bool, list[Any]] + :return: List of dig results. + :rtype: list[dict] """ - server = f"@{server}" if server and "@" not in server else "" - record_type = "AAAA" if ":" in address else "A" - args = f"{server} -x {address} PTR" if reverse else f"{server} {address} {record_type}" + server = f"@{server}" if server else "" + args = f"+norecurse {'-x ' if ip_is_valid(address) else ''}{address} {server}" - answers = jc.parse("dig", self.host.conn.run(f"dig {args}").stdout) + try: + output = self.host.conn.run(f"dig {args}").stdout + parsed_output = jc.parse("dig", output) + except Exception: + return None - if not isinstance(answers, list) or not answers: + if not isinstance(parsed_output, list) or not parsed_output: return None - result = answers[0].get("answer", []) - if not isinstance(result, list): + + result = parsed_output[0].get("answer", []) + if not isinstance(result, list) or not result: return None required_keys = {"name", "type", "ttl", "data"} records = [] for record in result: - if not isinstance(record, dict) or not required_keys.issubset(record.keys()): + if not isinstance(record, dict): + continue + if not required_keys.issubset(record.keys()): continue if not isinstance(record["ttl"], int) or record["ttl"] < 0: continue - records.append( - {"name": record["name"], "type": record["type"], "ttl": record["ttl"], "data": record["data"]} - ) - if len(records) <= 1 and len(result) != 0: - records[0]["all_data"] = records + # Strip trailing dots for easier matching + _name = record["name"].rstrip(".") if isinstance(record["name"], str) else record["name"] + _data = record["data"].rstrip(".") if isinstance(record["data"], str) else record["data"] - return None if not records else records[0] - - def nslookup(self, args: list[str]) -> ProcessResult: - """ - Execute nslookup command with given arguments. - - :param args: Arguments to ``nslookup``, defaults to None - :type args: list[str] - :return: SSH Process result - :rtype: ProcessResult - """ + records.append( + { + "name": _name, + "data": _data, + "type": record["type"], + "ttl": record["ttl"], + } + ) - return self.host.conn.exec(["nslookup", *args], raise_on_error=False) + return records if records else None def teardown(self): """ @@ -259,6 +256,9 @@ def _get_route(self) -> tuple[Any | None, Any | None]: :return: Default gateway device name and ip. :rtype: tuple[Any | None, Any | None] """ + device_name = None + device_ip = None + result = jc.parse("ip-route", self.host.conn.exec(["ip", "route"]).stdout) if isinstance(result, list): if isinstance(result[0], dict) and result[0]["ip"] == "default": From 077bfb82fe31410858a6602ae894e4d6dfd602ac Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Thu, 16 Apr 2026 15:29:09 -0400 Subject: [PATCH 3/7] fixing and improving dns remove records method --- sssd_test_framework/roles/ad.py | 54 ++++++++++++++++++++------- sssd_test_framework/roles/generic.py | 2 +- sssd_test_framework/roles/ipa.py | 41 +++++++++++++++++---- sssd_test_framework/roles/samba.py | 55 +++++++++++++++++++--------- 4 files changed, 112 insertions(+), 40 deletions(-) diff --git a/sssd_test_framework/roles/ad.py b/sssd_test_framework/roles/ad.py index 885756b2..be08889d 100644 --- a/sssd_test_framework/roles/ad.py +++ b/sssd_test_framework/roles/ad.py @@ -19,6 +19,7 @@ attrs_include_value, attrs_parse, attrs_to_hash, + ip_to_ptr, ip_version, seconds_to_timespan, ) @@ -2347,32 +2348,57 @@ def add_record(self, name: str, data: str | int) -> ADDNSZone: """ args = "" + if self.domain not in name: + name = f"{name}.{self.domain}" + short_name = name.split(".")[0] + if isinstance(data, int): - args = f"-Ptr -Name {str(data)} -AllowUpdateAny -PtrDomainName {name}.{self.zone_name}" + args = f"-Ptr -Name {str(data)} -AllowUpdateAny -PtrDomainName {name}." elif isinstance(data, str) and ip_version(data) == 4: - args = f"-A -Name {name} -IPv4Address {data}" + args = f"-A -Name {short_name} -IPv4Address {data}" elif isinstance(data, str) and ip_version(data) == 6: - args = f"-A -Name {name} -IPv6Address {data}" + args = f"-A -Name {short_name} -IPv6Address {data}" self.host.conn.run(f"Add-DnsServerResourceRecord -ZoneName {self.zone_name} {args} ") return self def delete_record(self, name: str) -> None: """ - Delete DNS record. + Delete DNS record, both forward and reverse records are deleted. - :param name: Name of the record. + :param name: Name or IP of the record. :type name: str """ - if "in-addr" in self.zone_name: - record_type = "PTR" - else: - data = self.host.conn.run(f"dig +short {name}").stdout.strip() - record_type = "AAAA" if ":" in data else "A" + if self.domain not in name: + name = f"{name}.{self.domain}" - self.host.conn.run( - f"Remove-DnsServerResourceRecord -ZoneName {self.zone_name} -Name {name} -RRType {record_type} -Force" - ) + records = self.host.conn.run(f"dig +short +norecurse {name} '@{self.server}'").stdout_lines + records = [s.rstrip("\r") for s in records] + + if not isinstance(records, list) or records is None: + return None + + if len(records) > 1: + for record in records: + if ip_version(record) == 4: + self.role.host.conn.run( + f"Remove-DnsServerResourceRecord -RRType A -Force -ZoneName {self.zone_name} -Name {name}" + ) + if ip_version(record) == 6: + self.host.conn.run( + f"Remove-DnsServerResourceRecord -RRType AAAA -Force -ZoneName {self.zone_name} -Name {name}" + ) + + for ptr_records in records: + ptr_record = self.host.conn.run(f"dig +short -x +norecurse {ptr_records} '@{self.server}'").stdout_lines + ptr_record = [r.rstrip("\r") for r in ptr_record] + if ptr_record: + self.host.conn.run( + f"Remove-DnsServerResourceRecord -RRType PTR " + f"-Force -ZoneName {ip_to_ptr(ptr_record[0])} " + f"-Name {'.'.join(ptr_record).split()[-1]}", + ) + return None def print(self) -> str: """ @@ -2381,7 +2407,7 @@ def print(self) -> str: :return: Print zone data. :rtype: str """ - return self.host.conn.run(f"Get-DnsServerResourceRecord -ZoneName {self.zone_name}").stdout + return self.host.conn.run(f"Get-DnsServerResourceRecord -ZoneName {self.zone_name} | Format-List").stdout ADNetgroupMember: TypeAlias = LDAPNetgroupMember[ADUser, ADNetgroup] diff --git a/sssd_test_framework/roles/generic.py b/sssd_test_framework/roles/generic.py index c06f68ea..d76b6096 100644 --- a/sssd_test_framework/roles/generic.py +++ b/sssd_test_framework/roles/generic.py @@ -1552,7 +1552,7 @@ def add_record(self, name: str, data: str | int) -> GenericDNSZone: @abstractmethod def delete_record(self, name: str) -> None: """ - Delete DNS record. + Delete DNS record, both forward and reverse records are deleted. :param name: Name of the record. :type name: str diff --git a/sssd_test_framework/roles/ipa.py b/sssd_test_framework/roles/ipa.py index a84e5ad0..cf40129c 100644 --- a/sssd_test_framework/roles/ipa.py +++ b/sssd_test_framework/roles/ipa.py @@ -438,7 +438,7 @@ def test_example(client: Client, ipa: IPA): :return: New host account object. :rtype: IPAHostAccount """ - return IPAHostAccount(self, name) + return IPAHostAccount(self, f"{name}.{self.domain}" if self.domain not in name else name) def sudorule(self, name: str) -> IPASudoRule: """ @@ -2867,7 +2867,8 @@ def create(self) -> IPADNSZone: :return: IPADNSServer object. :rtype: IPADNSServer """ - self.host.conn.run(f"ipa dnszone-add {self.zone_name} --dynamic-update=TRUE --skip-overlap-check") + self.host.conn.run(f"ipa dnszone-add {self.zone_name} --skip-overlap-check") + self.host.conn.run(f"ipa dnszone-mod {self.zone_name} --dynamic-update=TRUE --allow-sync-ptr=TRUE") return self def delete(self) -> None: @@ -2891,13 +2892,16 @@ def add_record(self, name: str, data: str | int) -> IPADNSZone: :rtype: IPADNSZone """ args = "" + if self.domain not in name: + name = f"{name}.{self.domain}" + short_name = name.split(".")[0] if isinstance(data, int): args = f"{str(data)} --ptr-rec={name}." elif isinstance(data, str) and ip_version(data) == 4: - args = f"{name} --a-rec={data}" + args = f"{short_name} --a-rec={data}" elif isinstance(data, str) and ip_version(data) == 6: - args = f"{name} --aaaa-rec={data}" + args = f"{short_name} --aaaa-rec={data}" self.host.conn.run(f"ipa dnsrecord-add {self.zone_name} {args}") @@ -2905,12 +2909,35 @@ def add_record(self, name: str, data: str | int) -> IPADNSZone: def delete_record(self, name: str) -> None: """ - Delete DNS record. + Delete DNS record, both forward and reverse records are deleted. - :param name: Name of the record. + :param name: Name. :type name: str """ - self.host.conn.run(f"ipa dnsrecord-del {self.zone_name} {name}") + if self.domain not in name: + name = f"{name}.{self.domain}" + + records = self.host.conn.run(f"dig +short +norecurse {name} '@{self.server}'").stdout_lines + records = [s.rstrip("\r") for s in records] + + if not isinstance(records, list) or records is None: + return None + + if len(records) > 1: + for record in records: + if ip_version(record) == 4: + self.host.conn.run(f"ipa dnsrecord-del {self.zone_name} --a-rec={record}") + if ip_version(record) == 6: + self.host.conn.run(f"ipa dnsrecord-del {self.zone_name} --aaaa-rec={record}") + + for ptr_records in records: + ptr_record = self.host.conn.run(f"dig +short -x +norecurse {ptr_records} '@{self.server}'").stdout_lines + ptr_record = [r.rstrip("\r") for r in ptr_record] + if ptr_record: + self.host.conn.run(f"ipa dnsrecord-del {self.zone_name} --ptr-rec={record}") + return None + + time.sleep(5) # Wait for the record to be deleted def print(self) -> str: """ diff --git a/sssd_test_framework/roles/samba.py b/sssd_test_framework/roles/samba.py index 0135c00b..11de0772 100644 --- a/sssd_test_framework/roles/samba.py +++ b/sssd_test_framework/roles/samba.py @@ -11,7 +11,7 @@ from pytest_mh.conn import ProcessResult from ..hosts.samba import SambaHost -from ..misc import attrs_parse, ip_version, to_list_of_strings +from ..misc import attrs_parse, ip_to_ptr, ip_version, to_list_of_strings from ..utils.ldap import LDAPRecordAttributes from .base import BaseLinuxLDAPRole, BaseObject, DeleteAttribute from .generic import GenericPasswordPolicy @@ -1360,37 +1360,56 @@ def add_record(self, name: str, data: str) -> SambaDNSZone: :rtype: SambaDNSZone """ args = "" + if self.domain not in name: + name = f"{name}.{self.domain}" + short_name = name.split(".")[0] if isinstance(data, int): - args = f" {name} PTR {str(data)} {self.credentials}" + args = f" {name}. PTR {str(data)} {self.credentials}" elif isinstance(data, str) and ip_version(data) == 4: - args = f" {name} A {data} {self.credentials}" + args = f" {short_name} A {data} {self.credentials}" elif isinstance(data, str) and ip_version(data) == 6: - args = f" {name} AAAA {data} {self.credentials}" + args = f" {short_name} AAAA {data} {self.credentials}" self.host.conn.run(f"samba-tool dns add {self.server} {self.zone_name} {args}") return self def delete_record(self, name: str) -> None: """ - Delete DNS record. + Delete DNS record, both forward and reverse records are deleted. :param name: Name of the record. :type name: str """ - if "in-addr" in self.zone_name: - record_type = "PTR" - data = self.host.conn.run(f"dig -x +short {name}").stdout.strip() - else: - data = self.host.conn.run(f"dig +short {name}").stdout.strip() - record_type = "AAAA" if ":" in data else "A" - - self.role.host.conn.run( - f"samba-tool dns delete " - f"{self.server} {self.zone_name} " - f"{name} {record_type} {data} " - f"{self.credentials}" - ) + if self.domain not in name: + name = f"{name}.{self.domain}" + + records = self.host.conn.run(f"dig +short +norecurse {name} '@{self.server}'").stdout_lines + records = [s.rstrip("\r") for s in records] + + if not isinstance(records, list) or records is None: + return None + + if len(records) > 1: + for record in records: + if ip_version(record) == 4: + self.role.host.conn.run( + f"samba-tool dns delete {self.server} {self.zone_name} {name} A {record} {self.credentials}" + ) + if ip_version(record) == 6: + self.host.conn.run( + f"samba-tool dns delete {self.server} {self.zone_name} {name} AAAA {record} {self.credentials}" + ) + + for ptr_records in records: + ptr_record = self.host.conn.run(f"dig +short -x +norecurse {ptr_records} '@{self.server}'").stdout_lines + ptr_record = [r.rstrip("\r") for r in ptr_record] + if ptr_record: + self.host.conn.run( + f"samba-tool dns delete {self.server} {ip_to_ptr(ptr_record[0])} {name} " + f"PTR {ptr_record} {self.credentials}" + ) + return None def print(self) -> str: """ From 2692c19336d6d73e31a5470ba0ec3097cfbda9ad Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Thu, 16 Apr 2026 15:29:26 -0400 Subject: [PATCH 4/7] pdating topology controller to update the client hostname * removing realm configuration to the sssd-ci-containers --- sssd_test_framework/topology_controllers.py | 41 ++++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/sssd_test_framework/topology_controllers.py b/sssd_test_framework/topology_controllers.py index f0e93fab..efebafc8 100644 --- a/sssd_test_framework/topology_controllers.py +++ b/sssd_test_framework/topology_controllers.py @@ -90,16 +90,6 @@ def join_domain(self, client: ClientHost, provider: IPAHost | ADHost | SambaHost client.fs.backup("/etc/ipa") client.fs.backup("/var/lib/ipa-client") - # Configure realm to keep kerberos intact - client.fs.backup("/etc/realmd.conf") - client.fs.write( - "/etc/realmd.conf", - """ - [service] - manage-krb5-conf = no - """, - ) - # Join provider domain result = client.conn.exec(["realm", "join", provider.domain], input=provider.adminpw, raise_on_error=False) if result.rc != 0: @@ -211,6 +201,15 @@ class IPATopologyController(ProvisionedBackupTopologyController): @BackupTopologyController.restore_vanilla_on_error def topology_setup(self, client: ClientHost, ipa: IPAHost) -> None: + short_hostname = client.conn.run("hostname").stdout.split(".")[0].strip() + hostname = f"{short_hostname}.{ipa.domain}" + + # Change client hostname to match the domain + self.logger.info(f"Changing hostname to {hostname}") + client.conn.run(f"hostname {hostname}") + + client.fs.backup("/etc/resolv.conf") + if self.provisioned: self.logger.info(f"Topology '{self.name}' is already provisioned") return @@ -229,6 +228,13 @@ class ADTopologyController(ProvisionedBackupTopologyController): @BackupTopologyController.restore_vanilla_on_error def topology_setup(self, client: ClientHost, provider: ADHost | SambaHost) -> None: + short_hostname = client.conn.run("hostname").stdout.split(".")[0].strip() + hostname = f"{short_hostname}.{provider.domain}" + + # Change client hostname to match the domain + self.logger.info(f"Changing hostname to {hostname}") + client.conn.run(f"hostname {hostname}") + if self.provisioned: self.logger.info(f"Topology '{self.name}' is already provisioned") return @@ -255,6 +261,13 @@ class IPATrustADTopologyController(ProvisionedBackupTopologyController): @BackupTopologyController.restore_vanilla_on_error def topology_setup(self, client: ClientHost, ipa: IPAHost, trusted: ADHost | SambaHost) -> None: + short_hostname = client.conn.run("hostname").stdout.split(".")[0].strip() + hostname = f"{short_hostname}.{ipa.domain}" + + # Change client hostname to match the domain + self.logger.info(f"Changing hostname to {hostname}") + client.conn.run(f"hostname {hostname}") + if self.provisioned: self.logger.info(f"Topology '{self.name}' is already provisioned") return @@ -387,6 +400,13 @@ class GDMTopologyController(ProvisionedBackupTopologyController): @BackupTopologyController.restore_vanilla_on_error def topology_setup(self, client: ClientHost, ipa: IPAHost, keycloak: KeycloakHost) -> None: + short_hostname = client.conn.run("hostname").stdout.split(".")[0].strip() + hostname = f"{short_hostname}.{keycloak.domain}" + + # Change client hostname to match the domain + self.logger.info(f"Changing hostname to {hostname}") + client.conn.run(f"hostname {hostname}") + if "gdm" not in client.features or not client.features["gdm"]: self.logger.info(f"Topology '{self.name}' setup skipped because gdm feature not found on client") return @@ -435,4 +455,5 @@ def topology_teardown(self, client: ClientHost, ipa: IPAHost, keycloak: Keycloak ipa.kinit() ipa.conn.run("ipa idp-del keycloak") + super().topology_teardown() From 911d3b333fefb98f82921fbf7bbe968d14c787c3 Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Mon, 20 Apr 2026 21:49:13 -0400 Subject: [PATCH 5/7] adding common configuration for dyndns --- sssd_test_framework/utils/sssd.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sssd_test_framework/utils/sssd.py b/sssd_test_framework/utils/sssd.py index 03208d54..4f77f8d9 100644 --- a/sssd_test_framework/utils/sssd.py +++ b/sssd_test_framework/utils/sssd.py @@ -972,6 +972,20 @@ def mkhomedir(self) -> None: self.sssd.authselect.select("sssd", ["with-mkhomedir"]) self.sssd.svc.start("oddjobd.service") + def dyndns(self, device: str = "dummy0") -> None: + """ + Configure SSSD for dynamic DNS. + + :param device: Network device, defaults to 'dummy0' + :type device: str + """ + self.sssd.domain["dyndns_update"] = "True" + # Note: The default value is False for IPA.The IPA server updates the PTR record itself. + self.sssd.domain["dyndns_update_ptr"] = "True" + self.sssd.domain["dyndns_iface"] = device + self.sssd.domain["dyndns_refresh_interval"] = "1" + self.sssd.domain["dyndns_refresh_interval_offset"] = "5" + def ldap_provider( self, server: str, From 231f04a1034d218d81ea4bcb8a2663c3433fdf26 Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Mon, 20 Apr 2026 21:49:26 -0400 Subject: [PATCH 6/7] updating topology controllers to update dns hostname --- sssd_test_framework/topology_controllers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sssd_test_framework/topology_controllers.py b/sssd_test_framework/topology_controllers.py index efebafc8..73ef1c07 100644 --- a/sssd_test_framework/topology_controllers.py +++ b/sssd_test_framework/topology_controllers.py @@ -203,6 +203,11 @@ class IPATopologyController(ProvisionedBackupTopologyController): def topology_setup(self, client: ClientHost, ipa: IPAHost) -> None: short_hostname = client.conn.run("hostname").stdout.split(".")[0].strip() hostname = f"{short_hostname}.{ipa.domain}" + client.fs.backup("/etc/hostname") + client.fs.backup("/etc/hosts") + client.conn.run(f"echo {hostname} > /etc/hostname") + client.fs.write("/etc/hosts", client.fs.read("/etc/hosts").replace("client.test", hostname)) + # Change client hostname to match the domain self.logger.info(f"Changing hostname to {hostname}") From 24932158fc8e34b840f8fc15d0f51ff1bbe24a89 Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Thu, 23 Apr 2026 15:16:08 -0400 Subject: [PATCH 7/7] ipa dns forwarders are no longer necessary --- sssd_test_framework/topology_controllers.py | 51 --------------------- 1 file changed, 51 deletions(-) diff --git a/sssd_test_framework/topology_controllers.py b/sssd_test_framework/topology_controllers.py index 73ef1c07..6129b27f 100644 --- a/sssd_test_framework/topology_controllers.py +++ b/sssd_test_framework/topology_controllers.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -import socket import tempfile from pytest_mh import BackupTopologyController @@ -208,7 +207,6 @@ def topology_setup(self, client: ClientHost, ipa: IPAHost) -> None: client.conn.run(f"echo {hostname} > /etc/hostname") client.fs.write("/etc/hosts", client.fs.read("/etc/hosts").replace("client.test", hostname)) - # Change client hostname to match the domain self.logger.info(f"Changing hostname to {hostname}") client.conn.run(f"hostname {hostname}") @@ -277,9 +275,6 @@ def topology_setup(self, client: ClientHost, ipa: IPAHost, trusted: ADHost | Sam self.logger.info(f"Topology '{self.name}' is already provisioned") return - # Configure DNS forwarder for AD domain on IPA server - self.setup_dns_forwarder(ipa, trusted) - # Create trust self.logger.info(f"Establishing trust between {ipa.domain} and {trusted.domain}") ipa.kinit() @@ -293,52 +288,6 @@ def topology_setup(self, client: ClientHost, ipa: IPAHost, trusted: ADHost | Sam # Backup so we can restore to this state after each test super().topology_setup() - def setup_dns_forwarder(self, ipa: IPAHost, trusted: ADHost | SambaHost) -> None: - """ - Configure DNS forwarder on IPA server for the trusted AD domain. - - This ensures IPA can resolve the AD domain for trust establishment. - """ - self.logger.info(f"Configuring DNS forwarder for {trusted.domain} on {ipa.hostname}") - ipa.kinit() - - # Check if forwarder already exists - result = ipa.conn.exec( - ["ipa", "dnsforwardzone-show", trusted.domain], - raise_on_error=False, - ) - - if result.rc == 0: - self.logger.info(f"DNS forwarder for {trusted.domain} already exists, skipping") - return - - # Resolve AD server hostname to IP address (forwarder requires IP) - # Use getattr to safely access the host attribute from the connection - ad_hostname = getattr(trusted.conn, "host", trusted.hostname) - try: - ad_ip = socket.gethostbyname(ad_hostname) - except socket.gaierror: - self.logger.error( - f"Could not resolve hostname '{ad_hostname}'. " - "Please ensure it is resolvable from the test controller." - ) - raise - - # Add DNS forward zone pointing to the AD server IP - ipa.conn.exec( - [ - "ipa", - "dnsforwardzone-add", - trusted.domain, - f"--forwarder={ad_ip}", - "--forward-policy=only", - ] - ) - - # Restart named to ensure it picks up the new forwarder zone - ipa.conn.exec(["systemctl", "restart", "named"]) - self.logger.info(f"DNS forwarder for {trusted.domain} configured successfully") - # If this command is run on freshly started containers, it is possible the IPA is not yet # fully ready to create the trust. It takes a while for it to start working. @retry_command(max_retries=20, delay=5, match_stderr='CIFS server communication error: code "3221225581"')