From 304d7011b54e37fb6ef4b8029f7d1a072ec924df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Thu, 21 Dec 2023 10:42:24 +0100 Subject: [PATCH] Test DHCP replies related to DNS settings This ensures we do not send any domain search options: - `option domain-search` - `option dhcp6.domain-search` See `man dhcp-options`. It also asserts that updates to the DNS servers of the subnet, are reflected in the DHCP replies shortly after. --- README.md | 1 + events.py | 6 +++++ resources.py | 21 ++++++++++++++++ test_private_network.py | 55 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f6858bf..e61699b 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ These tests are run regularly against our public infrastructure as well as our i | | [test_private_network_mtu](./test_private_network.py#L244) | default | | | [test_private_network_only_on_all_images](./test_private_network.py#L302) | all | | | [test_private_network_attach_later](./test_private_network.py#L324) | default | +| | [test_private_network_dhcp_dns_replies](./test_private_network.py#L358) | default | | **Public Network** | [test_public_ip_address_on_all_images](./test_public_network.py#L22) | all | | | [test_public_network_connectivity_on_all_images](./test_public_network.py#L51) | all | | | [test_public_network_mtu](./test_public_network.py#L70) | default | diff --git a/events.py b/events.py index df35532..0e8d760 100644 --- a/events.py +++ b/events.py @@ -434,6 +434,12 @@ def on_test_teardown(name, outcome): 'dns_servers': 'args.self.dns_servers', }) +track_in_event_log('subnet.change-dns-servers.after', include={ + **RESOURCE_ID, + **RESULT, + 'dns_servers': 'args.self.dns_servers', +}) + # Keep track of custom images track_in_event_log('custom-image.import.after', include={ diff --git a/resources.py b/resources.py index 70d6f09..42c72d5 100644 --- a/resources.py +++ b/resources.py @@ -665,6 +665,23 @@ def interface_name(self, floating_ip): return f'f-{digest}' + def dhcp_reply(self, interface_name, ip_version, timeout=2.5): + """ Starts a DHCP discovery on the interface and returns a reply, + without configuring the interface. + + This is used to assert DHCP replies and requires dhclient to be + installed. + + """ + return self.output_of(oneliner(f""" + sudo timeout {timeout}s dhclient {interface_name} + -{ip_version} + -lf /dev/stdout + -n -d -q -1 + --no-pid + 2>/dev/null || true + """)) + def configure_floating_ip(self, floating_ip): interface = self.interface_name(floating_ip) @@ -975,6 +992,10 @@ def __contains__(self, address): def create(self): self.info = self.api.post('/subnets', json=self.spec).json() + @with_trigger('subnet.change-dns-servers') + def change_dns_servers(self, dns_servers): + self.api.patch(self.href, json={'dns_servers': dns_servers}) + def delete(self): """ Subnets are not explicitly deleted as they are automatically removed when their network is removed. diff --git a/test_private_network.py b/test_private_network.py index f0087fb..343d2cf 100644 --- a/test_private_network.py +++ b/test_private_network.py @@ -27,7 +27,7 @@ def assert_a_private_address(): retry_for(seconds=5).or_warn(assert_a_private_address, msg=( f'{server.name}: No private IP address after 5s')) - # if this all together takes more than 30 seconds, we count it as a failure + # If this all together takes more than 30 seconds, we count it as a failure retry_for(seconds=25).or_fail(assert_a_private_address, msg=( f'{server.name}: No private IP address after 30s')) @@ -353,3 +353,56 @@ def assert_private_network_is_configured(): assert_private_network_is_configured, msg='Failed to configure private network.', ) + + +def test_private_network_dhcp_dns_replies(server, private_network): + """ Subnets contain a DHCP server, whose DNS server list can be + configured by the user. + + """ + + # Create a default subnet + subnet = private_network.add_subnet('10.1.2.0/24') + + server.update( + interfaces=[{"network": "public"}, + {"network": private_network.info["uuid"]}] + ) + + assert server.private_interface.exists + + # No DHCP reply sets a search domain + reply = server.dhcp_reply(server.public_interface.name, ip_version=4) + assert "domain-search" not in reply + + reply = server.dhcp_reply(server.public_interface.name, ip_version=6) + assert "domain-search" not in reply + + reply = server.dhcp_reply(server.private_interface.name, ip_version=4) + assert "domain-search" not in reply + + # The DNS server of the private subnet can be changed + subnet.change_dns_servers(['10.0.0.8', '10.0.0.9']) + + def assert_custom_dns_servers(): + reply = server.dhcp_reply(server.private_interface.name, ip_version=4) + assert "domain-name-servers 10.0.0.8,10.0.0.9" in reply + assert "domain-search" not in reply + + retry_for(seconds=10).or_fail( + assert_custom_dns_servers, + msg='Failed to configure custom DNS servers.', + ) + + # We can also use an empty set of DNS servers + subnet.change_dns_servers([]) + + def assert_no_dns_servers(): + reply = server.dhcp_reply(server.private_interface.name, ip_version=4) + assert "domain-name-servers" not in reply + assert "domain-search" not in reply + + retry_for(seconds=10).or_fail( + assert_no_dns_servers, + msg='Failed to configure an empty set of DNS servers.', + )