Merge ~cgrabowski/maas:backport_lp2056050_3.4 into maas:3.4

Proposed by Christian Grabowski
Status: Merged
Approved by: Björn Tillenius
Approved revision: 2813fddd11a7a573d793c0746cbc47565c5712b4
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~cgrabowski/maas:backport_lp2056050_3.4
Merge into: maas:3.4
Diff against target: 340 lines (+178/-11)
10 files modified
src/maasserver/dns/config.py (+1/-1)
src/maasserver/dns/tests/test_config.py (+4/-1)
src/maasserver/fields.py (+39/-0)
src/maasserver/forms/domain.py (+5/-5)
src/maasserver/forms/tests/test_domain.py (+24/-0)
src/maasserver/migrations/maasserver/0318_add_port_to_forward_dns_servers.py (+17/-0)
src/maasserver/models/forwarddnsserver.py (+12/-1)
src/maasserver/tests/test_fields.py (+49/-0)
src/provisioningserver/dns/tests/test_config.py (+26/-2)
src/provisioningserver/templates/dns/named.conf.template (+1/-1)
Reviewer Review Type Date Requested Status
Björn Tillenius Approve
MAAS Lander Approve
Review via email: mp+462712@code.launchpad.net

Commit message

fix: allow non-default ports for forward dns servers

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b backport_lp2056050_3.4 lp:~cgrabowski/maas/+git/maas into -b 3.4 lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 2813fddd11a7a573d793c0746cbc47565c5712b4

review: Approve
Revision history for this message
Björn Tillenius (bjornt) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/dns/config.py b/src/maasserver/dns/config.py
2index b03d47d..4709fe6 100644
3--- a/src/maasserver/dns/config.py
4+++ b/src/maasserver/dns/config.py
5@@ -60,7 +60,7 @@ def forward_domains_to_forwarded_zones(forward_domains):
6 (
7 domain.name,
8 [
9- fwd_dns_srvr.ip_address
10+ (fwd_dns_srvr.ip_address, fwd_dns_srvr.port)
11 for fwd_dns_srvr in domain.forward_dns_servers
12 ],
13 )
14diff --git a/src/maasserver/dns/tests/test_config.py b/src/maasserver/dns/tests/test_config.py
15index 44507a0..620cf44 100644
16--- a/src/maasserver/dns/tests/test_config.py
17+++ b/src/maasserver/dns/tests/test_config.py
18@@ -102,7 +102,10 @@ class TestDNSUtilities(MAASServerTestCase):
19 )
20 self.assertCountEqual(
21 fwd_zones,
22- [(name1, [fwd_srvr1.ip_address]), (name2, [fwd_srvr2.ip_address])],
23+ [
24+ (name1, [(fwd_srvr1.ip_address, fwd_srvr1.port)]),
25+ (name2, [(fwd_srvr2.ip_address, fwd_srvr2.port)]),
26+ ],
27 )
28
29
30diff --git a/src/maasserver/fields.py b/src/maasserver/fields.py
31index 661a4d6..f4bd280 100644
32--- a/src/maasserver/fields.py
33+++ b/src/maasserver/fields.py
34@@ -5,6 +5,7 @@
35
36 from itertools import chain
37 import re
38+import urllib
39
40 from django import forms
41 from django.core.exceptions import ObjectDoesNotExist, ValidationError
42@@ -332,6 +333,44 @@ class IPListFormField(forms.CharField):
43 return " ".join(ips)
44
45
46+class IPPortListFormField(IPListFormField):
47+ def __init__(self, default_port=None, *args, **kwargs):
48+ super().__init__(*args, **kwargs)
49+ self._default_port = default_port
50+
51+ def clean(self, value):
52+ if value is None:
53+ return None
54+ else:
55+ ip_ports = re.split(self.separators, value)
56+ ip_ports = [
57+ ip_port.strip() for ip_port in ip_ports if ip_ports != ""
58+ ]
59+ result = []
60+ for ip_port in ip_ports:
61+ if "." in ip_port or ("[" == ip_port[0] and "]:" in ip_port):
62+ sock_addr = urllib.parse.urlsplit("//" + ip_port)
63+ ip = sock_addr.hostname
64+ port = (
65+ int(sock_addr.port)
66+ if sock_addr.port
67+ else self._default_port
68+ )
69+ else:
70+ ip = ip_port
71+ port = self._default_port
72+ try:
73+ GenericIPAddressField().clean(ip, model_instance=None)
74+ IntegerField().clean(port, model_instance=None)
75+ except ValidationError:
76+ raise ValidationError(
77+ f"Invalid IP and port combination: {ip_port};"
78+ f"please provide a list of space-separated IP addresses {'and port' if not self._default_port else ''}"
79+ )
80+ result.append((ip, port))
81+ return result
82+
83+
84 class HostListFormField(forms.CharField):
85 """Accepts a space/comma separated list of hostnames or IP addresses.
86
87diff --git a/src/maasserver/forms/domain.py b/src/maasserver/forms/domain.py
88index b530f3f..d5739a4 100644
89--- a/src/maasserver/forms/domain.py
90+++ b/src/maasserver/forms/domain.py
91@@ -7,7 +7,7 @@
92 from django import forms
93 from django.core.exceptions import ValidationError
94
95-from maasserver.fields import IPListFormField
96+from maasserver.fields import IPPortListFormField
97 from maasserver.forms import APIEditMixin, MAASModelForm
98 from maasserver.models.domain import Domain
99 from maasserver.models.forwarddnsserver import ForwardDNSServer
100@@ -24,16 +24,16 @@ class DomainForm(MAASModelForm):
101
102 authoritative = forms.NullBooleanField(required=False)
103
104- forward_dns_servers = IPListFormField(required=False)
105+ forward_dns_servers = IPPortListFormField(default_port=53, required=False)
106
107 def save(self):
108 super(MAASModelForm, self).save()
109 fwd_srvrs = self.cleaned_data.get("forward_dns_servers")
110 if fwd_srvrs is not None:
111- fwd_srvrs_list = fwd_srvrs.split(" ")
112- for fwd_srvr_ip in fwd_srvrs_list:
113+ for fwd_srvr_ip, fwd_srvr_port in fwd_srvrs:
114 fwd_srvr = ForwardDNSServer.objects.get_or_create(
115- ip_address=fwd_srvr_ip
116+ ip_address=fwd_srvr_ip,
117+ port=fwd_srvr_port,
118 )[0]
119 fwd_srvr.domains.add(self.instance)
120 fwd_srvr.save()
121diff --git a/src/maasserver/forms/tests/test_domain.py b/src/maasserver/forms/tests/test_domain.py
122index 043dc47..e9a3a3e 100644
123--- a/src/maasserver/forms/tests/test_domain.py
124+++ b/src/maasserver/forms/tests/test_domain.py
125@@ -104,6 +104,30 @@ class TestDomainForm(MAASServerTestCase):
126 for fwd_dns_srvr in domain.forward_dns_servers
127 ],
128 )
129+ for fwd_dns_srvr in domain.forward_dns_servers:
130+ self.assertEqual(fwd_dns_srvr.port, 53)
131+
132+ def test_can_create_forward_dns_server_with_port(self):
133+ name = factory.make_name("domain")
134+ forward_dns_servers = [
135+ f"{factory.make_ip_address(ipv6=False)}:5353" for _ in range(0, 2)
136+ ]
137+ form = DomainForm(
138+ {
139+ "name": name,
140+ "authoritative": False,
141+ "forward_dns_servers": " ".join(forward_dns_servers),
142+ }
143+ )
144+ self.assertTrue(form.is_valid(), form.errors)
145+ domain = form.save()
146+ self.assertEqual(
147+ forward_dns_servers,
148+ [
149+ fwd_dns_srvr.ip_and_port
150+ for fwd_dns_srvr in domain.forward_dns_servers
151+ ],
152+ )
153
154 def test_validate_authority(self):
155 name = factory.make_name("domain")
156diff --git a/src/maasserver/migrations/maasserver/0318_add_port_to_forward_dns_servers.py b/src/maasserver/migrations/maasserver/0318_add_port_to_forward_dns_servers.py
157new file mode 100644
158index 0000000..322927f
159--- /dev/null
160+++ b/src/maasserver/migrations/maasserver/0318_add_port_to_forward_dns_servers.py
161@@ -0,0 +1,17 @@
162+# Generated by Django 3.2.12 on 2024-03-07 17:16
163+
164+from django.db import migrations, models
165+
166+
167+class Migration(migrations.Migration):
168+ dependencies = [
169+ ("maasserver", "0304_interface_params_no_autoconf"),
170+ ]
171+
172+ operations = [
173+ migrations.AddField(
174+ model_name="forwarddnsserver",
175+ name="port",
176+ field=models.IntegerField(default=53),
177+ ),
178+ ]
179diff --git a/src/maasserver/models/forwarddnsserver.py b/src/maasserver/models/forwarddnsserver.py
180index 5dcf740..64eb2e8 100644
181--- a/src/maasserver/models/forwarddnsserver.py
182+++ b/src/maasserver/models/forwarddnsserver.py
183@@ -6,7 +6,12 @@ __all__ = [
184 ]
185
186
187-from django.db.models import GenericIPAddressField, Manager, ManyToManyField
188+from django.db.models import (
189+ GenericIPAddressField,
190+ IntegerField,
191+ Manager,
192+ ManyToManyField,
193+)
194
195 from maasserver.models.cleansave import CleanSave
196 from maasserver.models.domain import Domain
197@@ -31,4 +36,10 @@ class ForwardDNSServer(CleanSave, TimestampedModel):
198 null=False, default=None, editable=False, unique=True
199 )
200
201+ port = IntegerField(null=False, default=53)
202+
203 domains = ManyToManyField(Domain)
204+
205+ @property
206+ def ip_and_port(self):
207+ return f"{self.ip_address}:{self.port}"
208diff --git a/src/maasserver/tests/test_fields.py b/src/maasserver/tests/test_fields.py
209index 643366f..7af6607 100644
210--- a/src/maasserver/tests/test_fields.py
211+++ b/src/maasserver/tests/test_fields.py
212@@ -12,6 +12,7 @@ from testtools import ExpectedException
213 from maasserver.fields import (
214 HostListFormField,
215 IPListFormField,
216+ IPPortListFormField,
217 LargeObjectField,
218 LargeObjectFile,
219 MODEL_NAME_VALIDATOR,
220@@ -304,6 +305,54 @@ class TestIPListFormField(MAASTestCase):
221 )
222
223
224+class TestIPPortListFormField(MAASTestCase):
225+ def test_accepts_ipv4_with_port(self):
226+ ips = [factory.make_ip_address(ipv6=False) for _ in range(5)]
227+ ip_ports = [f"{ip}:80" for ip in ips]
228+ input = ",".join(ip_ports)
229+ self.assertEqual(
230+ [(ip, 80) for ip in ips], IPPortListFormField().clean(input)
231+ )
232+
233+ def test_accepts_ipv4_without_port(self):
234+ ips = [factory.make_ip_address(ipv6=False) for _ in range(5)]
235+ input = ",".join(ips)
236+ self.assertEqual(
237+ [(ip, 80) for ip in ips],
238+ IPPortListFormField(default_port=80).clean(input),
239+ )
240+
241+ def test_accepts_ipv6_with_port(self):
242+ ips = [factory.make_ip_address(ipv6=True) for _ in range(5)]
243+ ip_ports = [f"[{ip}]:80" for ip in ips]
244+ input = ",".join(ip_ports)
245+ self.assertEqual(
246+ [(ip, 80) for ip in ips], IPPortListFormField().clean(input)
247+ )
248+
249+ def test_accepts_ipv6_without_port(self):
250+ ips = [factory.make_ip_address(ipv6=True) for _ in range(5)]
251+ input = ",".join(ips)
252+ self.assertEqual(
253+ [(ip, 80) for ip in ips],
254+ IPPortListFormField(default_port=80).clean(input),
255+ )
256+
257+ def test_rejects_invalid_ip(self):
258+ ip_ports = [
259+ f"{factory.make_ip_address(ipv6=False)}:80" for _ in range(4)
260+ ]
261+ invalid = f"{factory.make_name()}:80"
262+ ip_ports.append(invalid)
263+ input = ",".join(ip_ports)
264+ error = self.assertRaises(
265+ ValidationError, IPPortListFormField().clean, input
266+ )
267+ self.assertIn(
268+ f"Invalid IP and port combination: {invalid}", error.message
269+ )
270+
271+
272 class TestHostListFormField(MAASTestCase):
273 def test_accepts_none(self):
274 self.assertIsNone(HostListFormField().clean(None))
275diff --git a/src/provisioningserver/dns/tests/test_config.py b/src/provisioningserver/dns/tests/test_config.py
276index 8c57ce4..9c7ad2f 100644
277--- a/src/provisioningserver/dns/tests/test_config.py
278+++ b/src/provisioningserver/dns/tests/test_config.py
279@@ -601,7 +601,7 @@ class TestDNSConfig(MAASTestCase):
280 def test_write_config_with_forwarded_zones(self):
281 name = factory.make_name("domain")
282 ip = factory.make_ip_address()
283- forwarded_zones = [(name, [ip])]
284+ forwarded_zones = [(name, [(ip, None)])]
285 target_dir = patch_dns_config_path(self)
286 DNSConfig(forwarded_zones=forwarded_zones).write_config()
287 config_path = os.path.join(target_dir, MAAS_NAMED_CONF_NAME)
288@@ -611,7 +611,7 @@ class TestDNSConfig(MAASTestCase):
289 type forward;
290 forward only;
291 forwarders {{
292- {ip};
293+ {ip} port 53;
294 }};
295 }};
296 """
297@@ -620,6 +620,30 @@ class TestDNSConfig(MAASTestCase):
298 expected = parse_isc_string(expected_content)
299 self.assertEqual(expected[f'zone "{name}"'], config[f'zone "{name}"'])
300
301+ def test_write_config_with_forwarded_zones_with_custom_port(self):
302+ name = factory.make_name("domain")
303+ ip = factory.make_ip_address()
304+ port = 5353
305+ forwarded_zones = [(name, [(ip, port)])]
306+ target_dir = patch_dns_config_path(self)
307+ DNSConfig(forwarded_zones=forwarded_zones).write_config()
308+ config_path = os.path.join(target_dir, MAAS_NAMED_CONF_NAME)
309+ expected_content = dedent(
310+ f"""
311+ zone "{name}" {{
312+ type forward;
313+ forward only;
314+ forwarders {{
315+ {ip} port {port};
316+ }};
317+ }};
318+ """
319+ )
320+ config = read_isc_file(config_path)
321+ print(config)
322+ expected = parse_isc_string(expected_content)
323+ self.assertEqual(expected[f'zone "{name}"'], config[f'zone "{name}"'])
324+
325 def test_write_config_makes_config_world_readable(self):
326 target_dir = patch_dns_config_path(self)
327 DNSConfig().write_config()
328diff --git a/src/provisioningserver/templates/dns/named.conf.template b/src/provisioningserver/templates/dns/named.conf.template
329index 7217ca8..a909560 100644
330--- a/src/provisioningserver/templates/dns/named.conf.template
331+++ b/src/provisioningserver/templates/dns/named.conf.template
332@@ -21,7 +21,7 @@ zone "{{forwarded_zone[0]}}" {
333 forward only;
334 forwarders {
335 {{for forward_server in forwarded_zone[1]}}
336- {{forward_server}};
337+ {{forward_server[0]}} port {{ forward_server[1] if forward_server[1] else 53 }};
338 {{endfor}}
339 };
340 };

Subscribers

People subscribed via source and target branches