Merge ~andreserl/maas:lp1774206_trusted_acls into maas:master

Proposed by Andres Rodriguez
Status: Merged
Approved by: Andres Rodriguez
Approved revision: 864a786d09f692b7595d26aa4067728d024a61ea
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~andreserl/maas:lp1774206_trusted_acls
Merge into: maas:master
Diff against target: 515 lines (+311/-7)
11 files modified
src/maasserver/dns/config.py (+13/-2)
src/maasserver/dns/tests/test_config.py (+40/-0)
src/maasserver/fields.py (+64/-0)
src/maasserver/forms/__init__.py (+1/-0)
src/maasserver/forms/settings.py (+17/-0)
src/maasserver/models/config.py (+1/-0)
src/maasserver/tests/test_fields.py (+107/-0)
src/maasserver/triggers/system.py (+8/-4)
src/maasserver/triggers/tests/test_system_listener.py (+52/-0)
src/maasserver/views/tests/test_settings.py (+7/-1)
src/provisioningserver/dns/actions.py (+1/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Blake Rouse (community) Approve
Review via email: mp+347398@code.launchpad.net

Commit message

LP: #1774206 - Add config option 'dns_trusted_acl'.

MAAS DNS has a 'trusted' ACL that lists the networks that are allowed to use MAAS for DNS resolution. This option allows to specify other networks (or IPs/ACLs) not known to MAAS that can use it for DNS resolution.

To post a comment you must log in.
110fdce... by Andres Rodriguez

LP: #1774206 - Add ability to add extra items to the 'trusted' ACL

864a786... by Andres Rodriguez

Fix lint

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

UNIT TESTS
-b lp1774206_trusted_acls lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3586/console
COMMIT: 4de9e12c92cac6afccf19ebc5784ea63a6e97f46

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. Well tested to!

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

UNIT TESTS
-b lp1774206_trusted_acls lp:~andreserl/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 864a786d09f692b7595d26aa4067728d024a61ea

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 da2968e..e5dba64 100644
3--- a/src/maasserver/dns/config.py
4+++ b/src/maasserver/dns/config.py
5@@ -121,15 +121,26 @@ def get_dnssec_validation():
6 return Config.objects.get_config("dnssec_validation")
7
8
9+def get_trusted_acls():
10+ """Return the configuration option for trusted ACLs.
11+
12+ :return: A list of CIDR-format subnet, IPs or names.
13+ """
14+ items = Config.objects.get_config("dns_trusted_acl")
15+ return [] if items is None else items.split()
16+
17+
18 def get_trusted_networks():
19- """Return the CIDR representation of all the Subnets we know about.
20+ """Return the CIDR representation of all the subnets we know about
21+ combined with the list from get_trusted_acls().
22
23 :return: A list of CIDR-format subnet specifications.
24 """
25- return [
26+ known_subnets = [
27 str(subnet.cidr)
28 for subnet in Subnet.objects.all()
29 ]
30+ return list(set(known_subnets + get_trusted_acls()))
31
32
33 def get_resource_name_for_subnet(subnet):
34diff --git a/src/maasserver/dns/tests/test_config.py b/src/maasserver/dns/tests/test_config.py
35index 036c144..cd933fd 100644
36--- a/src/maasserver/dns/tests/test_config.py
37+++ b/src/maasserver/dns/tests/test_config.py
38@@ -19,6 +19,7 @@ from maasserver.dns.config import (
39 dns_update_all_zones,
40 get_internal_domain,
41 get_resource_name_for_subnet,
42+ get_trusted_acls,
43 get_trusted_networks,
44 get_upstream_dns,
45 )
46@@ -330,6 +331,17 @@ class TestDNSConfigModifications(TestDNSServer):
47 compose_config_path(DNSConfig.target_file_name),
48 FileContains(matcher=Contains(trusted_network)))
49
50+ def test_dns_update_all_zones_writes_trusted_networks_params_extra(self):
51+ self.patch(settings, 'DNS_CONNECT', True)
52+ extra_trusted_network = factory.make_ipv6_network()
53+ get_trusted_acls_patch = self.patch(
54+ dns_config_module, 'get_trusted_acls')
55+ get_trusted_acls_patch.return_value = [extra_trusted_network.cidr]
56+ dns_update_all_zones()
57+ self.assertThat(
58+ compose_config_path(DNSConfig.target_file_name),
59+ FileContains(matcher=Contains(str(extra_trusted_network))))
60+
61 def test_dns_config_has_NS_record(self):
62 self.patch(settings, 'DNS_CONNECT', True)
63 ip = factory.make_ipv4_address()
64@@ -443,6 +455,34 @@ class TestGetUpstreamDNS(MAASServerTestCase):
65 self.assertEqual(addresses, get_upstream_dns())
66
67
68+class TestGetTrustedAcls(MAASServerTestCase):
69+ """Test for maasserver/dns/config.py:get_trusted_acls()"""
70+
71+ def setUp(self):
72+ super(TestGetTrustedAcls, self).setUp()
73+ self.useFixture(RegionConfigurationFixture())
74+
75+ def test__returns_empty_string_if_no_networks(self):
76+ self.assertEqual([], get_trusted_acls())
77+
78+ def test__returns_single_network(self):
79+ subnet = factory.make_ipv6_network()
80+ Config.objects.set_config('dns_trusted_acl', str(subnet))
81+ expected = [str(subnet)]
82+ self.assertEqual(expected, get_trusted_acls())
83+
84+ def test__returns_many_networks(self):
85+ subnets = [
86+ str(factory.make_ipv4_network()) for _ in range(
87+ random.randint(1, 5))]
88+ actual_subnets = ' '.join(subnets)
89+ Config.objects.set_config('dns_trusted_acl', str(actual_subnets))
90+ expected = [subnet for subnet in subnets]
91+ # Note: This test was seen randomly failing because the networks were
92+ # in an unexpected order...
93+ self.assertItemsEqual(expected, get_trusted_acls())
94+
95+
96 class TestGetTrustedNetworks(MAASServerTestCase):
97 """Test for maasserver/dns/config.py:get_trusted_networks()"""
98
99diff --git a/src/maasserver/fields.py b/src/maasserver/fields.py
100index 2ff9a53..b139fa3 100644
101--- a/src/maasserver/fields.py
102+++ b/src/maasserver/fields.py
103@@ -690,6 +690,70 @@ class HostListFormField(forms.CharField):
104 return host
105
106
107+class SubnetListFormField(forms.CharField):
108+ """Accepts a space/comma separated list of hostnames, Subnets or IPs.
109+
110+ This field normalizes the list to a space-separated list.
111+ """
112+ separators = re.compile('[,\s]+')
113+
114+ # Regular expressions to sniff out things that look like IP addresses;
115+ # additional and more robust validation ought to be done to make sure.
116+ pt_ipv4 = r"(?: \d{1,3} [.] \d{1,3} [.] \d{1,3} [.] \d{1,3} )"
117+ pt_ipv6 = r"(?: (|[0-9A-Fa-f]{1,4}) [:] (|[0-9A-Fa-f]{1,4}) [:] (.*))"
118+ pt_ip = re.compile(
119+ r"^ (?: %s | %s ) $" % (pt_ipv4, pt_ipv6), re.VERBOSE)
120+ pt_subnet = re.compile(
121+ r"^ (?: %s | %s ) \/\d+$" % (pt_ipv4, pt_ipv6), re.VERBOSE)
122+
123+ def clean(self, value):
124+ if value is None:
125+ return None
126+ else:
127+ values = map(str.strip, self.separators.split(value))
128+ values = (value for value in values if len(value) != 0)
129+ values = map(self._clean_addr_or_host, values)
130+ return ' '.join(values)
131+
132+ def _clean_addr_or_host(self, value):
133+ looks_like_ip = self.pt_ip.match(value) is not None
134+ looks_like_subnet = self.pt_subnet.match(value) is not None
135+ if looks_like_subnet:
136+ return self._clean_subnet(value)
137+ elif looks_like_ip:
138+ return self._clean_addr(value)
139+ else:
140+ return self._clean_host(value)
141+
142+ def _clean_addr(self, value):
143+ try:
144+ addr = IPAddress(value)
145+ except ValueError:
146+ return
147+ except AddrFormatError as error:
148+ raise ValidationError(
149+ "Invalid IP address: %s." % value)
150+ else:
151+ return str(addr)
152+
153+ def _clean_subnet(self, value):
154+ try:
155+ cidr = IPNetwork(value)
156+ except AddrFormatError:
157+ raise ValidationError(
158+ "Invalid network: %s." % value)
159+ else:
160+ return str(cidr)
161+
162+ def _clean_host(self, host):
163+ try:
164+ validate_hostname(host)
165+ except ValidationError as error:
166+ raise ValidationError("Invalid hostname: " + error.message)
167+ else:
168+ return host
169+
170+
171 class CaseInsensitiveChoiceField(forms.ChoiceField):
172 """ChoiceField that allows the input to be case insensitive."""
173
174diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py
175index 6c20906..52939a3 100644
176--- a/src/maasserver/forms/__init__.py
177+++ b/src/maasserver/forms/__init__.py
178@@ -1501,6 +1501,7 @@ class DNSForm(ConfigForm):
179 """Settings page, DNS section."""
180 upstream_dns = get_config_field('upstream_dns')
181 dnssec_validation = get_config_field('dnssec_validation')
182+ dns_trusted_acl = get_config_field('dns_trusted_acl')
183
184
185 class NTPForm(ConfigForm):
186diff --git a/src/maasserver/forms/settings.py b/src/maasserver/forms/settings.py
187index 0420b33..a6ce778 100644
188--- a/src/maasserver/forms/settings.py
189+++ b/src/maasserver/forms/settings.py
190@@ -21,6 +21,7 @@ from maasserver.bootresources import IMPORT_RESOURCES_SERVICE_PERIOD
191 from maasserver.fields import (
192 HostListFormField,
193 IPListFormField,
194+ SubnetListFormField,
195 )
196 from maasserver.models import BootResource
197 from maasserver.models.config import (
198@@ -357,6 +358,22 @@ CONFIG_ITEMS = {
199 "provided by the set upstream DNS.")
200 }
201 },
202+ 'dns_trusted_acl': {
203+ 'default': None,
204+ 'form': SubnetListFormField,
205+ 'form_kwargs': {
206+ 'label': (
207+ "List of external networks (not previously known), that will "
208+ "be allowed to use MAAS for DNS resolution."),
209+ 'required': False,
210+ 'help_text': (
211+ "MAAS keeps a list of networks that are allowed to use MAAS "
212+ "for DNS resolution. This option allows to add extra "
213+ "networks (not previously known) to the trusted ACL where "
214+ "this list of networks is kept. It also supports specifying "
215+ "IPs or ACL names.")
216+ }
217+ },
218 'ntp_servers': {
219 'default': None,
220 'form': HostListFormField,
221diff --git a/src/maasserver/models/config.py b/src/maasserver/models/config.py
222index 4c86449..914313f 100644
223--- a/src/maasserver/models/config.py
224+++ b/src/maasserver/models/config.py
225@@ -78,6 +78,7 @@ def get_default_config():
226 # DNS settings
227 'upstream_dns': None,
228 'dnssec_validation': "auto",
229+ 'dns_trusted_acl': None,
230 'maas_internal_domain': 'maas-internal',
231 # NTP settings
232 'ntp_servers': 'ntp.ubuntu.com',
233diff --git a/src/maasserver/tests/test_fields.py b/src/maasserver/tests/test_fields.py
234index 10addd9..ffaab8e 100644
235--- a/src/maasserver/tests/test_fields.py
236+++ b/src/maasserver/tests/test_fields.py
237@@ -29,6 +29,7 @@ from maasserver.fields import (
238 MODEL_NAME_VALIDATOR,
239 NodeChoiceField,
240 register_mac_type,
241+ SubnetListFormField,
242 URLOrPPAFormField,
243 URLOrPPAValidator,
244 validate_mac,
245@@ -705,6 +706,112 @@ class TestNodeChoiceField(MAASServerTestCase):
246 self.assertEqual(node, node_field.clean(node.system_id))
247
248
249+class TestSubnetListFormField(MAASTestCase):
250+
251+ def test_accepts_none(self):
252+ self.assertIsNone(SubnetListFormField().clean(None))
253+
254+ def test_accepts_single_ip(self):
255+ ip = factory.make_ip_address()
256+ self.assertEqual(ip, SubnetListFormField().clean(ip))
257+
258+ def test_accepts_space_separated_ips(self):
259+ ips = [factory.make_ip_address() for _ in range(5)]
260+ input = ' '.join(ips)
261+ self.assertEqual(input, SubnetListFormField().clean(input))
262+
263+ def test_accepts_comma_separated_ips(self):
264+ ips = [factory.make_ip_address() for _ in range(5)]
265+ input = ','.join(ips)
266+ self.assertEqual(' '.join(ips), SubnetListFormField().clean(input))
267+
268+ def test_accepts_single_subnet(self):
269+ subnet = str(factory.make_ipv4_network())
270+ self.assertEqual(subnet, SubnetListFormField().clean(subnet))
271+
272+ def test_accepts_space_separated_subnets(self):
273+ subnets = [str(factory.make_ipv6_network()) for _ in range(5)]
274+ input = ' '.join(subnets)
275+ self.assertEqual(input, SubnetListFormField().clean(input))
276+
277+ def test_accepts_comma_separated_subnets(self):
278+ subnets = [str(factory.make_ipv4_network()) for _ in range(5)]
279+ input = ','.join(subnets)
280+ self.assertEqual(' '.join(subnets), SubnetListFormField().clean(input))
281+
282+ def test_separators_dont_conflict_with_ipv4_address(self):
283+ self.assertIsNone(re.search(
284+ SubnetListFormField.separators, factory.make_ipv4_address()))
285+
286+ def test_separators_dont_conflict_with_ipv6_address(self):
287+ self.assertIsNone(re.search(
288+ SubnetListFormField.separators, factory.make_ipv6_address()))
289+
290+ def test_accepts_hostname(self):
291+ hostname = factory.make_hostname()
292+ self.assertEqual(hostname, SubnetListFormField().clean(hostname))
293+
294+ def test_accepts_space_separated_hostnames(self):
295+ hostnames = factory.make_hostname(), factory.make_hostname()
296+ input = ' '.join(hostnames)
297+ self.assertEqual(input, SubnetListFormField().clean(input))
298+
299+ def test_accepts_comma_separated_hostnames(self):
300+ hostnames = factory.make_hostname(), factory.make_hostname()
301+ input = ','.join(hostnames)
302+ self.assertEqual(
303+ ' '.join(hostnames), SubnetListFormField().clean(input))
304+
305+ def test_accepts_misc(self):
306+ servers = {
307+ "::1",
308+ "1::",
309+ "1::2",
310+ "1:2::3",
311+ "1::2:3",
312+ "1:2::3:4",
313+ "::127.0.0.1",
314+ }
315+ input = ','.join(servers)
316+ self.assertEqual(' '.join(servers), SubnetListFormField().clean(input))
317+
318+ def test_rejects_invalid_ipv4_address(self):
319+ input = "%s 12.34.56.999" % factory.make_hostname()
320+ error = self.assertRaises(
321+ ValidationError, SubnetListFormField().clean, input)
322+ self.assertThat(error.message, Equals(
323+ "Invalid IP address: 12.34.56.999."))
324+
325+ def test_rejects_invalid_ipv6_address(self):
326+ input = "%s fe80::abcde" % factory.make_hostname()
327+ error = self.assertRaises(
328+ ValidationError, SubnetListFormField().clean, input)
329+ self.assertThat(error.message, Equals(
330+ "Invalid IP address: fe80::abcde."))
331+
332+ def test_rejects_invalid_ipv4_subnet(self):
333+ input = "%s 10.10.10.300/24" % factory.make_ipv4_network()
334+ error = self.assertRaises(
335+ ValidationError, SubnetListFormField().clean, input)
336+ self.assertThat(error.message, Equals(
337+ "Invalid network: 10.10.10.300/24."))
338+
339+ def test_rejects_invalid_ipv6_subnet(self):
340+ input = "%s 100::/300" % factory.make_ipv6_network()
341+ error = self.assertRaises(
342+ ValidationError, SubnetListFormField().clean, input)
343+ self.assertThat(error.message, Equals(
344+ "Invalid network: 100::/300."))
345+
346+ def test_rejects_invalid_hostname(self):
347+ input = "%s abc-.foo" % factory.make_hostname()
348+ error = self.assertRaises(
349+ ValidationError, SubnetListFormField().clean, input)
350+ self.assertThat(error.message, Equals(
351+ "Invalid hostname: Label cannot start or end with "
352+ "hyphen: 'abc-'."))
353+
354+
355 class TestVersionedTextFileField(MAASServerTestCase):
356
357 def test_creates_new(self):
358diff --git a/src/maasserver/triggers/system.py b/src/maasserver/triggers/system.py
359index d6df1d6..1db361c 100644
360--- a/src/maasserver/triggers/system.py
361+++ b/src/maasserver/triggers/system.py
362@@ -1335,8 +1335,8 @@ DNS_INTERFACE_UPDATE = dedent("""\
363
364 # Triggered when a config is inserted. Increments the zone serial and notifies
365 # that DNS needs to be updated. Only watches for inserts on config
366-# upstream_dns, dnssec_validation, default_dns_ttl, windows_kms_host, and
367-# maas_internal_domain.
368+# upstream_dns, dnssec_validation, default_dns_ttl, windows_kms_host,
369+# dns_trusted_acls and maas_internal_domain.
370 DNS_CONFIG_INSERT = dedent("""\
371 CREATE OR REPLACE FUNCTION sys_dns_config_insert()
372 RETURNS trigger as $$
373@@ -1344,6 +1344,7 @@ DNS_CONFIG_INSERT = dedent("""\
374 -- Only care about the
375 IF (NEW.name = 'upstream_dns' OR
376 NEW.name = 'dnssec_validation' OR
377+ NEW.name = 'dns_trusted_acl' OR
378 NEW.name = 'default_dns_ttl' OR
379 NEW.name = 'windows_kms_host' OR
380 NEW.name = 'maas_internal_domain')
381@@ -1359,15 +1360,18 @@ DNS_CONFIG_INSERT = dedent("""\
382
383 # Triggered when a config is updated. Increments the zone serial and notifies
384 # that DNS needs to be updated. Only watches for updates on config
385-# upstream_dns, dnssec_validation, default_dns_ttl, and windows_kms_host.
386+# upstream_dns, dnssec_validation, dns_trusted_acl, default_dns_ttl,
387+# and windows_kms_host.
388 DNS_CONFIG_UPDATE = dedent("""\
389 CREATE OR REPLACE FUNCTION sys_dns_config_update()
390 RETURNS trigger as $$
391 BEGIN
392- -- Only care about the
393+ -- Only care about the upstream_dns, default_dns_ttl,
394+ -- dns_trusted_acl and windows_kms_host.
395 IF (OLD.value != NEW.value AND (
396 NEW.name = 'upstream_dns' OR
397 NEW.name = 'dnssec_validation' OR
398+ NEW.name = 'dns_trusted_acl' OR
399 NEW.name = 'default_dns_ttl' OR
400 NEW.name = 'windows_kms_host' OR
401 NEW.name = 'maas_internal_domain'))
402diff --git a/src/maasserver/triggers/tests/test_system_listener.py b/src/maasserver/triggers/tests/test_system_listener.py
403index d00dd83..072f082 100644
404--- a/src/maasserver/triggers/tests/test_system_listener.py
405+++ b/src/maasserver/triggers/tests/test_system_listener.py
406@@ -4218,6 +4218,30 @@ class TestDNSConfigListener(
407
408 @wait_for_reactor
409 @inlineCallbacks
410+ def test_sends_message_for_config_dns_trusted_acl_insert(self):
411+ dns_trusted_acl_new = factory.make_name('internal')
412+ yield deferToDatabase(register_system_triggers)
413+ yield self.capturePublication()
414+ dv = DeferredValue()
415+ listener = self.make_listener_without_delay()
416+ listener.register(
417+ "sys_dns", lambda *args: dv.set(args))
418+ yield listener.startService()
419+ try:
420+ yield deferToDatabase(
421+ Config.objects.set_config,
422+ "dns_trusted_acl", dns_trusted_acl_new)
423+ yield dv.get(timeout=2)
424+ yield self.assertPublicationUpdated()
425+ finally:
426+ yield listener.stopService()
427+ self.assertThat(
428+ self.getCapturedPublication().source, Equals(
429+ "configuration dns_trusted_acl set to %s"
430+ % json.dumps(dns_trusted_acl_new)))
431+
432+ @wait_for_reactor
433+ @inlineCallbacks
434 def test_sends_message_for_config_upstream_dns_update(self):
435 upstream_dns_old = factory.make_ip_address()
436 upstream_dns_new = factory.make_ip_address()
437@@ -4326,6 +4350,34 @@ class TestDNSConfigListener(
438 "configuration maas_internal_domain changed to %s"
439 % (json.dumps(maas_internal_domain_new))))
440
441+ @wait_for_reactor
442+ @inlineCallbacks
443+ def test_sends_message_for_config_dns_trusted_acl_update(self):
444+ dns_trusted_acl_old = factory.make_name('internal')
445+ dns_trusted_acl_new = factory.make_name('internal_new')
446+ yield deferToDatabase(register_system_triggers)
447+ yield deferToDatabase(
448+ Config.objects.set_config,
449+ "dns_trusted_acl", dns_trusted_acl_old)
450+ yield self.capturePublication()
451+ dv = DeferredValue()
452+ listener = self.make_listener_without_delay()
453+ listener.register(
454+ "sys_dns", lambda *args: dv.set(args))
455+ yield listener.startService()
456+ try:
457+ yield deferToDatabase(
458+ Config.objects.set_config,
459+ "dns_trusted_acl", dns_trusted_acl_new)
460+ yield dv.get(timeout=2)
461+ yield self.assertPublicationUpdated()
462+ finally:
463+ yield listener.stopService()
464+ self.assertThat(
465+ self.getCapturedPublication().source, Equals(
466+ "configuration dns_trusted_acl changed to %s"
467+ % (json.dumps(dns_trusted_acl_new))))
468+
469
470 class TestDNSConfigListenerLegacy(
471 MAASLegacyTransactionServerTestCase, TransactionalHelpersMixin,
472diff --git a/src/maasserver/views/tests/test_settings.py b/src/maasserver/views/tests/test_settings.py
473index 392a84e..5670b18 100644
474--- a/src/maasserver/views/tests/test_settings.py
475+++ b/src/maasserver/views/tests/test_settings.py
476@@ -180,7 +180,10 @@ class SettingsTest(MAASServerTestCase):
477 self.addCleanup(bootsources.signals.enable)
478 bootsources.signals.disable()
479 self.client.login(user=factory.make_admin())
480- new_upstream = "8.8.8.8"
481+ new_upstream = "8.8.8.8 8.8.4.4"
482+ new_ipv4_subnet = factory.make_ipv4_network()
483+ new_ipv6_subnet = factory.make_ipv6_network()
484+ new_subnets = "%s %s" % (new_ipv4_subnet, new_ipv6_subnet)
485 response = self.client.post(
486 reverse('settings_network'),
487 get_prefixed_form_data(
488@@ -188,12 +191,15 @@ class SettingsTest(MAASServerTestCase):
489 data={
490 'upstream_dns': new_upstream,
491 'dnssec_validation': 'no',
492+ 'dns_trusted_acl': new_subnets,
493 }))
494 self.assertEqual(
495 http.client.FOUND, response.status_code, response.content)
496 self.assertEqual(
497 new_upstream, Config.objects.get_config('upstream_dns'))
498 self.assertEqual('no', Config.objects.get_config('dnssec_validation'))
499+ self.assertEqual(
500+ new_subnets, Config.objects.get_config('dns_trusted_acl'))
501
502 def test_settings_ntp_POST(self):
503 # Disable boot source cache signals.
504diff --git a/src/provisioningserver/dns/actions.py b/src/provisioningserver/dns/actions.py
505index 97ad042..a955023 100644
506--- a/src/provisioningserver/dns/actions.py
507+++ b/src/provisioningserver/dns/actions.py
508@@ -123,6 +123,7 @@ def bind_write_options(upstream_dns, dnssec_validation):
509 """Write BIND options.
510
511 :param upstream_dns: A sequence of upstream DNS servers.
512+ :param dnssec_validation: Whether to enable DNSSec.
513 """
514 # upstream_dns was formerly specified as a single IP address. These
515 # assertions are here to prevent code that assumes that slipping through.

Subscribers

People subscribed via source and target branches