Merge ~cgrabowski/maas:ability_to_nest_a_dhcp_snippet_in_a_pool into maas:master

Proposed by Christian Grabowski
Status: Merged
Approved by: Christian Grabowski
Approved revision: 051b4eabe55599c62413c6b2b9f6a6c8b4d3c4e3
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~cgrabowski/maas:ability_to_nest_a_dhcp_snippet_in_a_pool
Merge into: maas:master
Diff against target: 642 lines (+318/-9)
14 files modified
src/maasserver/api/dhcpsnippets.py (+3/-0)
src/maasserver/dhcp.py (+22/-2)
src/maasserver/forms/dhcpsnippet.py (+14/-1)
src/maasserver/forms/tests/test_dhcpsnippet.py (+70/-0)
src/maasserver/migrations/maasserver/0239_add_iprange_specific_dhcp_snippets.py (+24/-0)
src/maasserver/models/dhcpsnippet.py (+15/-0)
src/maasserver/models/tests/test_dhcpsnippet.py (+18/-0)
src/maasserver/testing/factory.py (+2/-0)
src/maasserver/tests/test_dhcp.py (+86/-4)
src/maasserver/websockets/handlers/tests/test_dhcpsnippet.py (+4/-0)
src/provisioningserver/dhcp/testing/config.py (+13/-2)
src/provisioningserver/rpc/cluster.py (+22/-0)
src/provisioningserver/templates/dhcp/dhcpd.conf.template (+13/-0)
src/provisioningserver/templates/dhcp/dhcpd6.conf.template (+12/-0)
Reviewer Review Type Date Requested Status
Björn Tillenius Approve
MAAS Lander Approve
Review via email: mp+402371@code.launchpad.net

Commit message

add ability to create DHCP snippets within a pool.

Description of the change

Adds the ability to create DHCP snippets within a pool.

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

UNIT TESTS
-b ability_to_nest_a_dhcp_snippet_in_a_pool lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/9977/console
COMMIT: a5f26c9496b47a52f6a345df8d74145019805a89

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b ability_to_nest_a_dhcp_snippet_in_a_pool lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/9979/console
COMMIT: a9bcdca000d7cfc56bfc0aab76628a09428c72e1

review: Needs Fixing
2a9f1f9... by Christian Grabowski

use a more test-fixture-friendly dhcp snippet

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b ability_to_nest_a_dhcp_snippet_in_a_pool lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/9981/console
COMMIT: 2a9f1f907c4a68c6f903332994141908050fc372

review: Needs Fixing
b7a04b2... by Christian Grabowski

use double quote in interpolated string

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b ability_to_nest_a_dhcp_snippet_in_a_pool lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: b7a04b201119a7919c21150eddc221455f922ceb

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

nice fix. I have a few comments/questions inline, but nothing major.

review: Needs Information
183f742... by Christian Grabowski

make pool dhcp_snippets param non-optional in making pools

cbb350e... by Christian Grabowski

filter on instance.subnet, not instance.subnet.id in form

051b4ea... by Christian Grabowski

add units asserting invalid arguments throw validation error

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b ability_to_nest_a_dhcp_snippet_in_a_pool lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 051b4eabe55599c62413c6b2b9f6a6c8b4d3c4e3

review: Approve
Revision history for this message
Christian Grabowski (cgrabowski) wrote :

Replied to a few comments, otherwise, just made the suggested changes.

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

Thanks. The changes look good, but there's still an issue with the RPC protocol changes you made.

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

As discussed on MM, I was confused, and the RPC changes are indeed needed.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/dhcpsnippets.py b/src/maasserver/api/dhcpsnippets.py
2index 71f3494..ed4fcb9 100644
3--- a/src/maasserver/api/dhcpsnippets.py
4+++ b/src/maasserver/api/dhcpsnippets.py
5@@ -262,6 +262,9 @@ class DHCPSnippetsHandler(OperationsHandler):
6 @param (string) "subnet" [required=false] The subnet this snippet
7 applies to. Cannot be used with node or global_snippet.
8
9+ @param (string) "iprange" [required=false] The iprange within a subnet
10+ this snippet applies to. Must also provide a subnet value.
11+
12 @param (boolean) "global_snippet" [required=false] Whether or not this
13 snippet is to be applied globally. Cannot be used with node or subnet.
14
15diff --git a/src/maasserver/dhcp.py b/src/maasserver/dhcp.py
16index 2622632..2d61705 100644
17--- a/src/maasserver/dhcp.py
18+++ b/src/maasserver/dhcp.py
19@@ -396,13 +396,14 @@ def make_hosts_for_subnets(subnets, nodes_dhcp_snippets: list = None):
20 return hosts
21
22
23-def make_pools_for_subnet(subnet, failover_peer=None):
24+def make_pools_for_subnet(subnet, dhcp_snippets, failover_peer=None):
25 """Return list of pools to create in the DHCP config for `subnet`."""
26 pools = []
27 for ip_range in subnet.get_dynamic_ranges().order_by("id"):
28 pool = {
29 "ip_range_low": ip_range.start_ip,
30 "ip_range_high": ip_range.end_ip,
31+ "dhcp_snippets": dhcp_snippets.get(ip_range.id, []),
32 }
33 if failover_peer is not None:
34 pool["failover_peer"] = failover_peer
35@@ -430,6 +431,7 @@ def make_subnet_config(
36 """
37 ip_network = subnet.get_ipnetwork()
38 dns_servers = []
39+ ipranges_dhcp_snippets = dict()
40 if subnet.allow_dns and default_dns_servers:
41 # If the MAAS DNS server is enabled make sure that is used first.
42 if subnet.gateway_ip:
43@@ -446,6 +448,20 @@ def make_subnet_config(
44 dns_servers += [IPAddress(server) for server in subnet.dns_servers]
45 if subnets_dhcp_snippets is None:
46 subnets_dhcp_snippets = []
47+ else:
48+ subnet_only_dhcp_snippets = []
49+ for snippet in subnets_dhcp_snippets:
50+ if snippet.iprange is not None:
51+ iprange_dhcp_snippets = ipranges_dhcp_snippets.get(
52+ snippet.iprange.id, []
53+ )
54+ iprange_dhcp_snippets.append(make_dhcp_snippet(snippet))
55+ ipranges_dhcp_snippets[
56+ snippet.iprange.id
57+ ] = iprange_dhcp_snippets
58+ else:
59+ subnet_only_dhcp_snippets.append(snippet)
60+ subnets_dhcp_snippets = subnet_only_dhcp_snippets
61
62 subnet_config = {
63 "subnet": str(ip_network.network),
64@@ -456,7 +472,11 @@ def make_subnet_config(
65 "dns_servers": dns_servers,
66 "ntp_servers": get_ntp_servers(ntp_servers, subnet, peer_rack),
67 "domain_name": default_domain.name,
68- "pools": make_pools_for_subnet(subnet, failover_peer),
69+ "pools": make_pools_for_subnet(
70+ subnet,
71+ ipranges_dhcp_snippets,
72+ failover_peer,
73+ ),
74 "dhcp_snippets": [
75 make_dhcp_snippet(dhcp_snippet)
76 for dhcp_snippet in subnets_dhcp_snippets
77diff --git a/src/maasserver/forms/dhcpsnippet.py b/src/maasserver/forms/dhcpsnippet.py
78index fc4d74b..12f14b2 100644
79--- a/src/maasserver/forms/dhcpsnippet.py
80+++ b/src/maasserver/forms/dhcpsnippet.py
81@@ -14,7 +14,7 @@ from maasserver.fields import (
82 VersionedTextFileField,
83 )
84 from maasserver.forms import MAASModelForm
85-from maasserver.models import DHCPSnippet, Node, Subnet
86+from maasserver.models import DHCPSnippet, IPRange, Node, Subnet
87 from maasserver.utils.forms import set_form_error
88 from provisioningserver.events import EVENT_TYPES
89
90@@ -57,6 +57,14 @@ class DHCPSnippetForm(MAASModelForm):
91 help_text="The subnet which the DHCP snippet is for.",
92 )
93
94+ iprange = SpecifierOrModelChoiceField(
95+ label="IP Range",
96+ queryset=IPRange.objects.all(),
97+ required=False,
98+ initial=None,
99+ help_text="The iprange which the DHCP snippet is for.",
100+ )
101+
102 global_snippet = forms.BooleanField(
103 label="Global DHCP Snippet",
104 required=False,
105@@ -75,6 +83,7 @@ class DHCPSnippetForm(MAASModelForm):
106 "enabled",
107 "node",
108 "subnet",
109+ "iprange",
110 "global_snippet",
111 )
112
113@@ -88,6 +97,10 @@ class DHCPSnippetForm(MAASModelForm):
114 self.fields["value"].initial = self.instance.value
115 if instance is not None and instance.node is not None:
116 self.initial["node"] = self.instance.node.system_id
117+ if instance is not None and instance.subnet is not None:
118+ self.fields["iprange"].queryset = IPRange.objects.filter(
119+ subnet=instance.subnet
120+ )
121
122 def clean(self):
123 cleaned_data = super().clean()
124diff --git a/src/maasserver/forms/tests/test_dhcpsnippet.py b/src/maasserver/forms/tests/test_dhcpsnippet.py
125index 2000f04..693e104 100644
126--- a/src/maasserver/forms/tests/test_dhcpsnippet.py
127+++ b/src/maasserver/forms/tests/test_dhcpsnippet.py
128@@ -129,6 +129,76 @@ class TestDHCPSnippetForm(MAASServerTestCase):
129 self.assertEqual(enabled, dhcp_snippet.enabled)
130 self.assertEqual(subnet, dhcp_snippet.subnet)
131
132+ def test_create_dhcp_snippet_with_iprange(self):
133+ subnet = factory.make_ipv4_Subnet_with_IPRanges()
134+ iprange = subnet.get_dynamic_ranges().first()
135+ iprange.save()
136+ name = factory.make_name("name")
137+ value = factory.make_string()
138+ description = factory.make_string()
139+ enabled = factory.pick_bool()
140+ form = DHCPSnippetForm(
141+ data={
142+ "name": name,
143+ "value": value,
144+ "description": description,
145+ "enabled": enabled,
146+ "subnet": subnet.id,
147+ "iprange": iprange.id,
148+ }
149+ )
150+ self.assertTrue(form.is_valid(), form.errors)
151+ endpoint = factory.pick_choice(ENDPOINT_CHOICES)
152+ request = HttpRequest()
153+ request.user = factory.make_User()
154+ dhcp_snippet = form.save(endpoint, request)
155+ self.assertEqual(name, dhcp_snippet.name)
156+ self.assertEqual(value, dhcp_snippet.value.data)
157+ self.assertEqual(description, dhcp_snippet.description)
158+ self.assertEqual(enabled, dhcp_snippet.enabled)
159+ self.assertEqual(subnet, dhcp_snippet.subnet)
160+ self.assertEqual(iprange, dhcp_snippet.iprange)
161+
162+ def test_create_dhcp_snippet_with_iprange_requires_subnet(self):
163+ subnet = factory.make_ipv4_Subnet_with_IPRanges()
164+ iprange = subnet.get_dynamic_ranges().first()
165+ iprange.save()
166+ name = factory.make_name("name")
167+ value = factory.make_string()
168+ description = factory.make_string()
169+ enabled = factory.pick_bool()
170+ form = DHCPSnippetForm(
171+ data={
172+ "name": name,
173+ "value": value,
174+ "dscription": description,
175+ "enabled": enabled,
176+ "iprange": iprange.id,
177+ }
178+ )
179+ self.assertFalse(form.is_valid(), form.errors)
180+
181+ def test_create_dhcp_snippet_with_iprange_requires_parent_subnet(self):
182+ subnet = factory.make_ipv4_Subnet_with_IPRanges()
183+ subnet2 = factory.make_Subnet()
184+ iprange = subnet.get_dynamic_ranges().first()
185+ iprange.save()
186+ name = factory.make_name("name")
187+ value = factory.make_string()
188+ description = factory.make_string()
189+ enabled = factory.pick_bool()
190+ form = DHCPSnippetForm(
191+ data={
192+ "name": name,
193+ "value": value,
194+ "dscription": description,
195+ "enabled": enabled,
196+ "subnet": subnet2.id,
197+ "iprange": iprange.id,
198+ }
199+ )
200+ self.assertFalse(form.is_valid(), form.errors)
201+
202 def test_cannt_create_dhcp_snippet_with_node_and_subnet(self):
203 node = factory.make_Node()
204 subnet = factory.make_Subnet()
205diff --git a/src/maasserver/migrations/maasserver/0239_add_iprange_specific_dhcp_snippets.py b/src/maasserver/migrations/maasserver/0239_add_iprange_specific_dhcp_snippets.py
206new file mode 100644
207index 0000000..f5bba86
208--- /dev/null
209+++ b/src/maasserver/migrations/maasserver/0239_add_iprange_specific_dhcp_snippets.py
210@@ -0,0 +1,24 @@
211+# Generated by Django 2.2.12 on 2021-05-06 19:28
212+
213+from django.db import migrations, models
214+import django.db.models.deletion
215+
216+
217+class Migration(migrations.Migration):
218+
219+ dependencies = [
220+ ("maasserver", "0238_disable_boot_architectures"),
221+ ]
222+
223+ operations = [
224+ migrations.AddField(
225+ model_name="dhcpsnippet",
226+ name="iprange",
227+ field=models.ForeignKey(
228+ blank=True,
229+ null=True,
230+ on_delete=django.db.models.deletion.CASCADE,
231+ to="maasserver.IPRange",
232+ ),
233+ ),
234+ ]
235diff --git a/src/maasserver/models/dhcpsnippet.py b/src/maasserver/models/dhcpsnippet.py
236index 61346d3..7a401d7 100644
237--- a/src/maasserver/models/dhcpsnippet.py
238+++ b/src/maasserver/models/dhcpsnippet.py
239@@ -14,6 +14,7 @@ from django.db.models import (
240 )
241
242 from maasserver.models.cleansave import CleanSave
243+from maasserver.models.iprange import IPRange
244 from maasserver.models.node import Node
245 from maasserver.models.subnet import Subnet
246 from maasserver.models.timestampedmodel import TimestampedModel
247@@ -85,6 +86,8 @@ class DHCPSnippet(CleanSave, TimestampedModel):
248
249 subnet = ForeignKey(Subnet, null=True, blank=True, on_delete=CASCADE)
250
251+ iprange = ForeignKey(IPRange, null=True, blank=True, on_delete=CASCADE)
252+
253 objects = DHCPSnippetManager()
254
255 def __str__(self):
256@@ -97,3 +100,15 @@ class DHCPSnippet(CleanSave, TimestampedModel):
257 "A DHCP snippet cannot be enabled on a node and subnet at the "
258 "same time."
259 )
260+ if self.iprange is not None and self.subnet is None:
261+ raise ValidationError(
262+ "A DHCP snippet cannot be enabled on an iprange without"
263+ "a parent subnet"
264+ )
265+ elif (
266+ self.iprange is not None
267+ and self.iprange.subnet_id != self.subnet.id
268+ ):
269+ raise ValidationError(
270+ "A DHCP snippet's IP Range must be within the" "parent subnet"
271+ )
272diff --git a/src/maasserver/models/tests/test_dhcpsnippet.py b/src/maasserver/models/tests/test_dhcpsnippet.py
273index 35cbb91..acefc80 100644
274--- a/src/maasserver/models/tests/test_dhcpsnippet.py
275+++ b/src/maasserver/models/tests/test_dhcpsnippet.py
276@@ -55,6 +55,24 @@ class TestDHCPSnippet(MAASServerTestCase):
277 self.assertEqual(enabled, dhcp_snippet.enabled)
278 self.assertEqual(subnet, dhcp_snippet.subnet)
279
280+ def test_factory_make_DHCPSnippet_sets_iprange(self):
281+ name = factory.make_name("dhcp_snippet")
282+ value = VersionedTextFile.objects.create(data=factory.make_string())
283+ description = factory.make_string()
284+ enabled = factory.pick_bool()
285+ subnet = factory.make_ipv4_Subnet_with_IPRanges()
286+ iprange = subnet.get_dynamic_ranges().first()
287+ iprange.save()
288+ dhcp_snippet = factory.make_DHCPSnippet(
289+ name, value, description, enabled, subnet=subnet, iprange=iprange
290+ )
291+ self.assertEqual(name, dhcp_snippet.name)
292+ self.assertEqual(value.data, dhcp_snippet.value.data)
293+ self.assertEqual(description, dhcp_snippet.description)
294+ self.assertEqual(enabled, dhcp_snippet.enabled)
295+ self.assertEqual(subnet, dhcp_snippet.subnet)
296+ self.assertEqual(iprange, dhcp_snippet.iprange)
297+
298 def test_can_only_set_snippet_for_node_or_subnet(self):
299 node = factory.make_Node()
300 subnet = factory.make_Subnet()
301diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py
302index c463876..929c159 100644
303--- a/src/maasserver/testing/factory.py
304+++ b/src/maasserver/testing/factory.py
305@@ -2955,6 +2955,7 @@ class Factory(maastesting.factory.Factory):
306 enabled=True,
307 node=None,
308 subnet=None,
309+ iprange=None,
310 ):
311 if name is None:
312 name = self.make_name("dhcp_snippet")
313@@ -2969,6 +2970,7 @@ class Factory(maastesting.factory.Factory):
314 enabled=enabled,
315 node=node,
316 subnet=subnet,
317+ iprange=iprange,
318 )
319
320 def make_default_PackageRepositories(self):
321diff --git a/src/maasserver/tests/test_dhcp.py b/src/maasserver/tests/test_dhcp.py
322index 6bc3db5..040bfbf 100644
323--- a/src/maasserver/tests/test_dhcp.py
324+++ b/src/maasserver/tests/test_dhcp.py
325@@ -1582,8 +1582,16 @@ class TestMakeSubnetConfig(MAASServerTestCase):
326 )
327 self.assertEqual(
328 [
329- {"ip_range_low": "10.9.8.11", "ip_range_high": "10.9.8.20"},
330- {"ip_range_low": "10.9.8.21", "ip_range_high": "10.9.8.30"},
331+ {
332+ "ip_range_low": "10.9.8.11",
333+ "ip_range_high": "10.9.8.20",
334+ "dhcp_snippets": [],
335+ },
336+ {
337+ "ip_range_low": "10.9.8.21",
338+ "ip_range_high": "10.9.8.30",
339+ "dhcp_snippets": [],
340+ },
341 ],
342 config["pools"],
343 )
344@@ -1615,11 +1623,13 @@ class TestMakeSubnetConfig(MAASServerTestCase):
345 "ip_range_low": "10.9.8.11",
346 "ip_range_high": "10.9.8.20",
347 "failover_peer": failover_peer,
348+ "dhcp_snippets": [],
349 },
350 {
351 "ip_range_low": "10.9.8.21",
352 "ip_range_high": "10.9.8.30",
353 "failover_peer": failover_peer,
354+ "dhcp_snippets": [],
355 },
356 ],
357 config["pools"],
358@@ -1696,6 +1706,58 @@ class TestMakeSubnetConfig(MAASServerTestCase):
359 config["disabled_boot_architectures"],
360 )
361
362+ def test_returns_iprange_dhcp_snippets(self):
363+ rack_controller = factory.make_RackController(interface=False)
364+ vlan = factory.make_VLAN()
365+ subnet = factory.make_ipv4_Subnet_with_IPRanges(vlan=vlan)
366+ iprange = subnet.get_dynamic_ranges().first()
367+ iprange.save()
368+ factory.make_Interface(
369+ INTERFACE_TYPE.PHYSICAL, vlan=vlan, node=rack_controller
370+ )
371+ default_domain = Domain.objects.get_default_domain()
372+ subnet_snippets = [
373+ factory.make_DHCPSnippet(subnet=subnet, enabled=True)
374+ for _ in range(3)
375+ ]
376+ iprange_snippets = [
377+ factory.make_DHCPSnippet(
378+ subnet=subnet, iprange=iprange, enabled=True
379+ )
380+ for _ in range(3)
381+ ]
382+ dhcp_snippets = subnet_snippets + iprange_snippets
383+ config = dhcp.make_subnet_config(
384+ rack_controller,
385+ subnet,
386+ [factory.make_ipv4_address()],
387+ [factory.make_name("ntp")],
388+ default_domain,
389+ subnets_dhcp_snippets=dhcp_snippets,
390+ )
391+ self.assertItemsEqual(
392+ [
393+ {
394+ "name": dhcp_snippet.name,
395+ "description": dhcp_snippet.description,
396+ "value": dhcp_snippet.value.data,
397+ }
398+ for dhcp_snippet in subnet_snippets
399+ ],
400+ config["dhcp_snippets"],
401+ )
402+ self.assertItemsEqual(
403+ [
404+ {
405+ "name": dhcp_snippet.name,
406+ "description": dhcp_snippet.description,
407+ "value": dhcp_snippet.value.data,
408+ }
409+ for dhcp_snippet in iprange_snippets
410+ ],
411+ config["pools"][0]["dhcp_snippets"],
412+ )
413+
414 def test_subnet_without_gateway_restricts_nameservers(self):
415 network1 = IPNetwork("10.9.8.0/24")
416 network2 = IPNetwork("10.9.9.0/24")
417@@ -2192,6 +2254,7 @@ class TestGetDHCPConfigureFor(MAASServerTestCase):
418 "ip_range_high": str(ip_range.end_ip),
419 "failover_peer": "failover-vlan-%d"
420 % ha_vlan.id,
421+ "dhcp_snippets": [],
422 }
423 for ip_range in (
424 ha_subnet.get_dynamic_ranges().order_by("id")
425@@ -2226,6 +2289,7 @@ class TestGetDHCPConfigureFor(MAASServerTestCase):
426 "ip_range_high": str(ip_range.end_ip),
427 "failover_peer": "failover-vlan-%d"
428 % ha_vlan.id,
429+ "dhcp_snippets": [],
430 }
431 for ip_range in (
432 other_subnet.get_dynamic_ranges().order_by(
433@@ -2350,6 +2414,7 @@ class TestGetDHCPConfigureFor(MAASServerTestCase):
434 "ip_range_high": str(ip_range.end_ip),
435 "failover_peer": "failover-vlan-%d"
436 % ha_vlan.id,
437+ "dhcp_snippets": [],
438 }
439 for ip_range in (
440 ha_subnet.get_dynamic_ranges().order_by("id")
441@@ -2379,6 +2444,7 @@ class TestGetDHCPConfigureFor(MAASServerTestCase):
442 "ip_range_high": str(ip_range.end_ip),
443 "failover_peer": "failover-vlan-%d"
444 % ha_vlan.id,
445+ "dhcp_snippets": [],
446 }
447 for ip_range in (
448 other_subnet.get_dynamic_ranges().order_by(
449@@ -2531,7 +2597,9 @@ class TestConfigureDHCP(MAASTransactionServerTestCase):
450 gateway_ip="fd38:c341:27da:c831::1",
451 dns_servers=[],
452 )
453- factory.make_IPRange(
454+ iprange_v4 = subnet_v4.get_dynamic_ranges().first()
455+ iprange_v4.save()
456+ iprange_v6 = factory.make_IPRange(
457 subnet_v6,
458 "fd38:c341:27da:c831:0:1::",
459 "fd38:c341:27da:c831:0:1:ffff:0",
460@@ -2561,6 +2629,12 @@ class TestConfigureDHCP(MAASTransactionServerTestCase):
461 )
462
463 for _ in range(3):
464+ factory.make_DHCPSnippet(
465+ subnet=subnet_v4, iprange=iprange_v4, enabled=True
466+ )
467+ factory.make_DHCPSnippet(
468+ subnet=subnet_v6, iprange=iprange_v6, enabled=True
469+ )
470 factory.make_DHCPSnippet(subnet=subnet_v4, enabled=True)
471 factory.make_DHCPSnippet(subnet=subnet_v6, enabled=True)
472 factory.make_DHCPSnippet(enabled=True)
473@@ -2831,7 +2905,9 @@ class TestValidateDHCPConfig(MAASTransactionServerTestCase):
474 subnet_v6 = factory.make_Subnet(
475 vlan=vlan, cidr="fd38:c341:27da:c831::/64"
476 )
477- factory.make_IPRange(
478+ iprange_v4 = subnet_v4.get_dynamic_ranges().first()
479+ iprange_v4.save()
480+ iprange_v6 = factory.make_IPRange(
481 subnet_v6,
482 "fd38:c341:27da:c831:0:1::",
483 "fd38:c341:27da:c831:0:1:ffff:0",
484@@ -2859,6 +2935,12 @@ class TestValidateDHCPConfig(MAASTransactionServerTestCase):
485 )
486
487 for _ in range(3):
488+ factory.make_DHCPSnippet(
489+ subnet=subnet_v4, iprange=iprange_v4, enabled=True
490+ )
491+ factory.make_DHCPSnippet(
492+ subnet=subnet_v6, iprange=iprange_v6, enabled=True
493+ )
494 factory.make_DHCPSnippet(subnet=subnet_v4, enabled=True)
495 factory.make_DHCPSnippet(subnet=subnet_v6, enabled=True)
496 factory.make_DHCPSnippet(enabled=True)
497diff --git a/src/maasserver/websockets/handlers/tests/test_dhcpsnippet.py b/src/maasserver/websockets/handlers/tests/test_dhcpsnippet.py
498index c1f7b34..39567d5 100644
499--- a/src/maasserver/websockets/handlers/tests/test_dhcpsnippet.py
500+++ b/src/maasserver/websockets/handlers/tests/test_dhcpsnippet.py
501@@ -31,8 +31,11 @@ class TestDHCPSnippetHandler(MAASServerTestCase):
502 def dehydrate_dhcp_snippet(self, dhcp_snippet):
503 node_system_id = None
504 subnet_id = None
505+ iprange_id = None
506 if dhcp_snippet.subnet is not None:
507 subnet_id = dhcp_snippet.subnet.id
508+ if dhcp_snippet.iprange is not None:
509+ iprange_id = dhcp_snippet.iprange.id
510 elif dhcp_snippet.node is not None:
511 node_system_id = dhcp_snippet.node.system_id
512 return {
513@@ -51,6 +54,7 @@ class TestDHCPSnippetHandler(MAASServerTestCase):
514 "enabled": dhcp_snippet.enabled,
515 "node": node_system_id,
516 "subnet": subnet_id,
517+ "iprange": iprange_id,
518 "updated": dehydrate_datetime(dhcp_snippet.updated),
519 "created": dehydrate_datetime(dhcp_snippet.created),
520 }
521diff --git a/src/provisioningserver/dhcp/testing/config.py b/src/provisioningserver/dhcp/testing/config.py
522index 0a2b82a..f44363d 100644
523--- a/src/provisioningserver/dhcp/testing/config.py
524+++ b/src/provisioningserver/dhcp/testing/config.py
525@@ -26,17 +26,23 @@ def fix_shared_networks_failover(shared_networks, failover_peers):
526 return shared_networks
527
528
529-def make_subnet_pool(network, start_ip=None, end_ip=None, failover_peer=None):
530+def make_subnet_pool(
531+ network, start_ip=None, end_ip=None, failover_peer=None, dhcp_snippets=None
532+):
533 """Return a pool entry for a subnet from network."""
534 if start_ip is None and end_ip is None:
535 start_ip, end_ip = factory.make_ip_range(network)
536 if failover_peer is None:
537 failover_peer = factory.make_name("failover")
538- return {
539+ if dhcp_snippets is None:
540+ dhcp_snippets = make_pool_dhcp_snippets()
541+ pool = {
542 "ip_range_low": str(start_ip),
543 "ip_range_high": str(end_ip),
544 "failover_peer": failover_peer,
545+ "dhcp_snippets": dhcp_snippets,
546 }
547+ return pool
548
549
550 def _make_snippets(count, template):
551@@ -66,6 +72,11 @@ def make_host_dhcp_snippets(allow_empty=True):
552 return _make_snippets(count, "option smtp-server %s;")
553
554
555+def make_pool_dhcp_snippets(allow_empty=True):
556+ count = random.randrange((0 if allow_empty else 1), 3)
557+ return _make_snippets(count, "option nntp-server %s;")
558+
559+
560 def make_host(
561 hostname=None,
562 interface_name=None,
563diff --git a/src/provisioningserver/rpc/cluster.py b/src/provisioningserver/rpc/cluster.py
564index a3fda58..b245c61 100644
565--- a/src/provisioningserver/rpc/cluster.py
566+++ b/src/provisioningserver/rpc/cluster.py
567@@ -347,6 +347,28 @@ class _ConfigureDHCP(amp.Command):
568 b"failover_peer",
569 amp.Unicode(optional=True),
570 ),
571+ (
572+ b"dhcp_snippets",
573+ AmpList(
574+ [
575+ (
576+ b"name",
577+ amp.Unicode(),
578+ ),
579+ (
580+ b"description",
581+ amp.Unicode(
582+ optional=True
583+ ),
584+ ),
585+ (
586+ b"value",
587+ amp.Unicode(),
588+ ),
589+ ],
590+ optional=True,
591+ ),
592+ ),
593 ]
594 ),
595 ),
596diff --git a/src/provisioningserver/templates/dhcp/dhcpd.conf.template b/src/provisioningserver/templates/dhcp/dhcpd.conf.template
597index 06c886d..a9d85ff 100644
598--- a/src/provisioningserver/templates/dhcp/dhcpd.conf.template
599+++ b/src/provisioningserver/templates/dhcp/dhcpd.conf.template
600@@ -115,6 +115,19 @@ shared-network {{shared_network["name"]}} {
601 {{if pool.get('failover_peer')}}
602 failover peer "{{pool['failover_peer']}}";
603 {{endif}}
604+
605+ {{if len(pool['dhcp_snippets']) == 0}}
606+ # No DHCP snippets for pool
607+ {{endif}}
608+ {{for dhcp_snippet in pool['dhcp_snippets']}}
609+ {{if dhcp_snippet['description'] != ''}}
610+ # Description: {{dhcp_snippet['description'] | oneline}}
611+ {{endif}}
612+ {{for line in dhcp_snippet['value'].splitlines()}}
613+ {{line}}
614+ {{endfor}}
615+ {{endfor}}
616+
617 range {{pool['ip_range_low']}} {{pool['ip_range_high']}};
618 }
619 {{endfor}}
620diff --git a/src/provisioningserver/templates/dhcp/dhcpd6.conf.template b/src/provisioningserver/templates/dhcp/dhcpd6.conf.template
621index 4a854d3..b33a839 100644
622--- a/src/provisioningserver/templates/dhcp/dhcpd6.conf.template
623+++ b/src/provisioningserver/templates/dhcp/dhcpd6.conf.template
624@@ -81,6 +81,18 @@ shared-network {{shared_network["name"]}} {
625
626 {{for pool in dhcp_subnet['pools']}}
627 pool6 {
628+ {{if len(pool['dhcp_snippets']) == 0}}
629+ # No DHCP snippets for pool
630+ {{endif}}
631+ {{for dhcp_snippet in pool['dhcp_snippets']}}
632+ {{if dhcp_snippet['description'] != ''}}
633+ # Description: {{dhcp_snippet['description'] | oneline}}
634+ {{endif}}
635+ {{for line in dhcp_snippet['value'].splitlines()}}
636+ {{line}}
637+ {{endfor}}
638+ {{endfor}}
639+
640 range6 {{pool['ip_range_low']}} {{pool['ip_range_high']}};
641 }
642 {{endfor}}

Subscribers

People subscribed via source and target branches