Merge lp:~julian-edwards/maas/upstream-dns into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 1729
Proposed branch: lp:~julian-edwards/maas/upstream-dns
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 440 lines (+153/-5)
14 files modified
etc/maas/templates/dns/named.conf.options.inside.maas.template (+6/-0)
src/maasserver/dns.py (+4/-1)
src/maasserver/dns_connect.py (+9/-0)
src/maasserver/forms.py (+1/-0)
src/maasserver/forms_settings.py (+14/-0)
src/maasserver/management/commands/set_up_dns.py (+5/-0)
src/maasserver/models/config.py (+1/-0)
src/maasserver/tests/test_dns.py (+16/-2)
src/maastesting/bindfixture.py (+8/-1)
src/maastesting/tests/test_bindfixture.py (+19/-0)
src/provisioningserver/dns/config.py (+32/-0)
src/provisioningserver/dns/tests/test_config.py (+29/-0)
src/provisioningserver/tasks.py (+3/-0)
src/provisioningserver/tests/test_tasks.py (+6/-1)
To merge this branch: bzr merge lp:~julian-edwards/maas/upstream-dns
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+193893@code.launchpad.net

Commit message

Have a DNS forwarders setting ("upstream DNS" in the settings UI) which is written to a special file that can be included in the DNS options {} block.

Description of the change

Add a new Config setting for upstream DNS ("forwarders" in the DNS config) which is written out to a file called named.conf.options.inside.maas from the similarly-named template. The file can then be used in an 'include' directive from the options {} block in the system's DNS config.

This branch turned out to be much bigger than expected because you can't have more than one options block (which would have been easy to just throw in our existing template for DNS). I got this technique from LaMont who set it up this way for his Puppet stuff.

I've also amended the BindFixture so it can take this option although it doesn't use it anywhere when you "make run", it's only set up in tests.

We will also need to make a change in the packaging so that the new file gets included in the named.conf.options file.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (4.7 KiB)

The branch as a whole looks great, with lots of helpful explanation. It brings out the simplicity in what, in the wrong hands, could have been short but incomprehensible logic. As always though, I have remarks!

First off, I have a small suggestion for the UI label in src/maasserver/forms_settings.py (form_kwargs, ll. 81—88 of the diff):

            'label': "Upstream DNS used to resolve non-MAAS domains",

Instead of "non-MAAS domains" you could say something like "domains not managed by this MAAS," to avoid confusion? For example I imagine a beginning user might mistake "non-MAAS domains" for a way of saying "DNS domains, not domains as in MAAS domain controller."

.

In src/maastesting/tests/test_bindfixture.py, I don't get what test_setUp_honours_include_in_options does with “forwarders” and “expected_in_file” (ll. 255—258 of the diff):

        forwarders = "forwarders { 1.2.3.4; };"
….
            expected_in_file = (
                resources.homedir + '/' + forwarders.encode("ascii"))

It looks as if you're doing effectively an os.path.join() on a directory and the actual DNS options text. Didn't you mean for “forwarders” to look like a filename?

Also, I don't know about resources.homedir, but '/' is unicode and forwarders.encode("ascii") is bytes, right? What's the point of the encoding? If you mean to produce the whole thing as bytes, why not encode the whole thing at once with os.path.join(...).encode('ascii')? If resources.homedir is still raw bytes in python3, we can probably fix it in the BIND fixture.

.

In src/provisioningserver/dns/config.py, set_up_options_conf (ll. 290—317 of the diff):

    """Write out the named.conf.options.inside.maas file.

    This file should be included by the top-level named.conf.options
    inside its 'options' block. MAAS cannot write the options file itself,
    so relies on either the DNSFixture in the test suite, or the packaging.
    Both should set that file up appropriately to include our file.
    """

Only a small nitpick, but I'd lead with the production situation ("MAAS cannot write the options file itself, so relies on the packaging") and make the equivalent test rigging parenthetical.

And to make up for that nitpick, here are a few tricks to offload some work from this function:

    template_path = os.path.join(
        locate_config(TEMPLATES_DIR),
        "named.conf.options.inside.maas.template")

...can be simplified to:

    template_path = locate_config(
        TEMPLATES_DIR, "named.conf.options.inside.maas.template")

Next there's...

    with open(template_path, "r") as f:
        template = tempita.Template(f.read(), name=template_path)

Tempita has a Template factory called from_filename which does the reading for you, and also passes the filename as the template name.

So try simply:

    template = tempita.Template.from_filename(template_path)

.

In src/provisioningserver/dns/tests/test_config.py, test_set_up_options_conf_writes_configuration hard-codes an IP address (l. 353 of the diff):

        fake_dns = "1.2.3.4"

We have a factory method for that! Use getRandomIPAddress().

.

In that same file, test_set_up_options_conf_raises_on_bad_template does thi...

Read more...

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :
Download full text (5.1 KiB)

Thanks for the review Mr V.

On 08/11/13 15:21, Jeroen T. Vermeulen wrote:
> Instead of "non-MAAS domains" you could say something like "domains not managed by this MAAS," to avoid confusion? For example I imagine a beginning user might mistake "non-MAAS domains" for a way of saying "DNS domains, not domains as in MAAS domain controller."

Ok changed, thanks.

> In src/maastesting/tests/test_bindfixture.py, I don't get what test_setUp_honours_include_in_options does with “forwarders” and “expected_in_file” (ll. 255—258 of the diff):
>
> forwarders = "forwarders { 1.2.3.4; };"
> ….
> expected_in_file = (
> resources.homedir + '/' + forwarders.encode("ascii"))
>
> It looks as if you're doing effectively an os.path.join() on a directory and the actual DNS options text. Didn't you mean for “forwarders” to look like a filename?

It's checking text exactly as it should appear in the file -
os.path.join would be inappropriate here.

> Also, I don't know about resources.homedir, but '/' is unicode and forwarders.encode("ascii") is bytes, right? What's the point of the encoding? If you mean to produce the whole thing as bytes, why not encode the whole thing at once with os.path.join(...).encode('ascii')? If resources.homedir is still raw bytes in python3, we can probably fix it in the BIND fixture.

Grar, that's a leftover from fiddling, I fixed it. Thanks for spotting.

> In src/provisioningserver/dns/config.py, set_up_options_conf (ll. 290—317 of the diff):
>
> """Write out the named.conf.options.inside.maas file.
>
> This file should be included by the top-level named.conf.options
> inside its 'options' block. MAAS cannot write the options file itself,
> so relies on either the DNSFixture in the test suite, or the packaging.
> Both should set that file up appropriately to include our file.
> """
>
> Only a small nitpick, but I'd lead with the production situation ("MAAS cannot write the options file itself, so relies on the packaging") and make the equivalent test rigging parenthetical.

Mind if I ignore that? Mostly because I'm not sure what you want and it
makes perfect sense already :)

> And to make up for that nitpick, here are a few tricks to offload some work from this function:
>
> template_path = os.path.join(
> locate_config(TEMPLATES_DIR),
> "named.conf.options.inside.maas.template")
>
> ...can be simplified to:
>
> template_path = locate_config(
> TEMPLATES_DIR, "named.conf.options.inside.maas.template")

You sure about that?

>
> Next there's...
>
> with open(template_path, "r") as f:
> template = tempita.Template(f.read(), name=template_path)
>
> Tempita has a Template factory called from_filename which does the reading for you, and also passes the filename as the template name.
>
> So try simply:
>
> template = tempita.Template.from_filename(template_path)

Right! Fixed.

>
> .
>
> In src/provisioningserver/dns/tests/test_config.py, test_set_up_options_conf_writes_configuration hard-codes an IP address (l. 353 of the diff):
>
> fake_dns = "1.2.3.4"
>
> We have a factory method for that! Use ge...

Read more...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (14.9 KiB)

The attempt to merge lp:~julian-edwards/maas/upstream-dns into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com saucy-security InRelease
Get:1 http://security.ubuntu.com saucy-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com saucy-security Release [49.6 kB]
Ign http://nova.clouds.archive.ubuntu.com saucy InRelease
Ign http://nova.clouds.archive.ubuntu.com saucy-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com saucy Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com saucy-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com saucy Release
Get:4 http://nova.clouds.archive.ubuntu.com saucy-updates Release [49.6 kB]
Get:5 http://security.ubuntu.com saucy-security/main Sources [7,939 B]
Get:6 http://security.ubuntu.com saucy-security/universe Sources [4,315 B]
Hit http://nova.clouds.archive.ubuntu.com saucy/main Sources
Hit http://nova.clouds.archive.ubuntu.com saucy/universe Sources
Get:7 http://security.ubuntu.com saucy-security/main amd64 Packages [32.4 kB]
Hit http://nova.clouds.archive.ubuntu.com saucy/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com saucy/universe amd64 Packages
Get:8 http://security.ubuntu.com saucy-security/universe amd64 Packages [9,325 B]
Hit http://nova.clouds.archive.ubuntu.com saucy/main Translation-en
Hit http://security.ubuntu.com saucy-security/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com saucy/universe Translation-en
Hit http://security.ubuntu.com saucy-security/universe Translation-en
Ign http://security.ubuntu.com saucy-security/main Translation-en_US
Get:9 http://nova.clouds.archive.ubuntu.com saucy-updates/main Sources [28.0 kB]
Ign http://security.ubuntu.com saucy-security/universe Translation-en_US
Get:10 http://nova.clouds.archive.ubuntu.com saucy-updates/universe Sources [8,509 B]
Get:11 http://nova.clouds.archive.ubuntu.com saucy-updates/main amd64 Packages [70.5 kB]
Get:12 http://nova.clouds.archive.ubuntu.com saucy-updates/universe amd64 Packages [28.1 kB]
Hit http://nova.clouds.archive.ubuntu.com saucy-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com saucy-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com saucy/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com saucy/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com saucy-updates/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com saucy-updates/universe Translation-en_US
Fetched 290 kB in 0s (1,411 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 avahi-daemon avahi-utils bind9 bind9utils build-essential curl daemontools distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make postgresql-9.1 python-amqplib python-avahi python-bzrlib python-celery python-convoy python-cssselect python-curtin python-dbus python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-formencode python-httplib2 python-jinja2 python-lockfile python-lxml pyth...

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Idiot linter.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Julian wrote:

> > In src/maastesting/tests/test_bindfixture.py, I don't get what test_setUp_honours_include_in_options does with “forwarders” and “expected_in_file” (ll. 255—258 of the diff):
> >
> > forwarders = "forwarders { 1.2.3.4; };"
> > ….
> > expected_in_file = (
> > resources.homedir + '/' + forwarders.encode("ascii"))
> >
> > It looks as if you're doing effectively an os.path.join() on a directory and the actual DNS options text. Didn't you mean for “forwarders” to look like a filename?
>
> It's checking text exactly as it should appear in the file -
> os.path.join would be inappropriate here.

Not my point. My point is that you seem to be conflating the text of the snippet that you want to include, with the path to the file that contains the snippet. You're constructing something like

    /tmp/tmp4SANQ84/forwarders { 1.2.3.4; };

Not what you meant, right? AFAICS you meant "forwarders" to look like a filename, not a piece of configuration.

.

> > template_path = os.path.join(
> > locate_config(TEMPLATES_DIR),
> > "named.conf.options.inside.maas.template")
> >
> > ...can be simplified to:
> >
> > template_path = locate_config(
> > TEMPLATES_DIR, "named.conf.options.inside.maas.template")
>
> You sure about that?

Quite sure, thank you.

.

> > And finally,
> >
> > 436 FileExists(),
> > 437 FileExists(),
> > 438 FileExists(),
> > 439 + FileExists(),
> >
> > WTF? :-)
>
> It's one of those crazy mass-checks that Raphers is so fond of. I just
> made it a bit bigger :)

And therefore, better.

Jeroen

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 08/11/13 17:41, Jeroen T. Vermeulen wrote:
> Julian wrote:
>
>>> In src/maastesting/tests/test_bindfixture.py, I don't get what test_setUp_honours_include_in_options does with “forwarders” and “expected_in_file” (ll. 255—258 of the diff):
>>>
>>> forwarders = "forwarders { 1.2.3.4; };"
>>> ….
>>> expected_in_file = (
>>> resources.homedir + '/' + forwarders.encode("ascii"))
>>>
>>> It looks as if you're doing effectively an os.path.join() on a directory and the actual DNS options text. Didn't you mean for “forwarders” to look like a filename?
>>
>> It's checking text exactly as it should appear in the file -
>> os.path.join would be inappropriate here.
>
> Not my point. My point is that you seem to be conflating the text of the snippet that you want to include, with the path to the file that contains the snippet. You're constructing something like
>
> /tmp/tmp4SANQ84/forwarders { 1.2.3.4; };
>
> Not what you meant, right? AFAICS you meant "forwarders" to look like a filename, not a piece of configuration.

No - I want it to look like configuration. You're the one who's
conflating two things here:

 1. The include file path
 2. The contents of the included file

The snippet you have above from test_bindfixture.py is very misleading,
remember that there's TWO files I am dealing with; the top-level options
that *includes* the other file which contains the "forwarders" option.

>
> .
>
>>> template_path = os.path.join(
>>> locate_config(TEMPLATES_DIR),
>>> "named.conf.options.inside.maas.template")
>>>
>>> ...can be simplified to:
>>>
>>> template_path = locate_config(
>>> TEMPLATES_DIR, "named.conf.options.inside.maas.template")
>>
>> You sure about that?
>
> Quite sure, thank you.

Why does the rest of the code need to use it then? It takes into
account the environment to override the template location.

> And therefore, better.

\m/

Or something…

:)

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 11/11/13 09:42, Julian Edwards wrote:
> No - I want it to look like configuration. You're the one who's
> conflating two things here:
>
> 1. The include file path
> 2. The contents of the included file
>
> The snippet you have above from test_bindfixture.py is very misleading,
> remember that there's TWO files I am dealing with; the top-level options
> that *includes* the other file which contains the "forwarders" option.

Ok as soon as I write this I see why I sent you up the garden path....

forwarders = "forwarders { 1.2.3.4; };"

should read:

forwarders = "named.conf.options.inside.maas".

I will fix this in a subsequent branch. Sorry…

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'etc/maas/templates/dns/named.conf.options.inside.maas.template'
2--- etc/maas/templates/dns/named.conf.options.inside.maas.template 1970-01-01 00:00:00 +0000
3+++ etc/maas/templates/dns/named.conf.options.inside.maas.template 2013-11-08 06:27:21 +0000
4@@ -0,0 +1,6 @@
5+{{if upstream_dns}}
6+ forwarders {
7+ {{upstream_dns}};
8+ };
9+{{endif}}
10+
11
12=== modified file 'src/maasserver/dns.py'
13--- src/maasserver/dns.py 2013-10-18 16:57:37 +0000
14+++ src/maasserver/dns.py 2013-11-08 06:27:21 +0000
15@@ -37,6 +37,7 @@
16 )
17 from maasserver.exceptions import MAASException
18 from maasserver.models import (
19+ Config,
20 DHCPLease,
21 NodeGroup,
22 NodeGroupInterface,
23@@ -312,7 +313,9 @@
24 if not write_conf:
25 return
26 zones = ZoneGenerator(NodeGroup.objects.all()).as_list()
27+ upstream_dns = Config.objects.get_config("upstream_dns")
28 tasks.write_full_dns_config.delay(
29 zones=zones,
30 callback=tasks.rndc_command.subtask(
31- args=[['reload'], reload_retry]))
32+ args=[['reload'], reload_retry]),
33+ upstream_dns=upstream_dns)
34
35=== modified file 'src/maasserver/dns_connect.py'
36--- src/maasserver/dns_connect.py 2013-10-07 09:12:40 +0000
37+++ src/maasserver/dns_connect.py 2013-11-08 06:27:21 +0000
38@@ -23,6 +23,7 @@
39 from django.dispatch import receiver
40 from maasserver.enum import NODEGROUPINTERFACE_MANAGEMENT
41 from maasserver.models import (
42+ Config,
43 Node,
44 NodeGroup,
45 NodeGroupInterface,
46@@ -88,3 +89,11 @@
47
48
49 connect_to_field_change(dns_post_edit_hostname_Node, Node, 'hostname')
50+
51+
52+def upstream_dns_changed(sender, instance, created, **kwargs):
53+ from maasserver.dns import write_full_dns_config
54+ write_full_dns_config()
55+
56+
57+Config.objects.config_changed_connect("upstream_dns", upstream_dns_changed)
58
59=== modified file 'src/maasserver/forms.py'
60--- src/maasserver/forms.py 2013-10-15 11:51:48 +0000
61+++ src/maasserver/forms.py 2013-11-08 06:27:21 +0000
62@@ -626,6 +626,7 @@
63 maas_name = get_config_field('maas_name')
64 enlistment_domain = get_config_field('enlistment_domain')
65 http_proxy = get_config_field('http_proxy')
66+ upstream_dns = get_config_field('upstream_dns')
67
68
69 class CommissioningForm(ConfigForm):
70
71=== modified file 'src/maasserver/forms_settings.py'
72--- src/maasserver/forms_settings.py 2013-10-07 09:12:40 +0000
73+++ src/maasserver/forms_settings.py 2013-11-08 06:27:21 +0000
74@@ -170,6 +170,20 @@
75 "proxy).")
76 }
77 },
78+ 'upstream_dns': {
79+ 'default': None,
80+ 'form': forms.GenericIPAddressField,
81+ 'form_kwargs': {
82+ 'label': (
83+ "Upstream DNS used to resolve domains not managed by this "
84+ "MAAS"),
85+ 'required': False,
86+ 'help_text': (
87+ "Only used when MAAS is running its own DNS server. This "
88+ "value is used as the value of 'forwarders' in the DNS "
89+ "server config.")
90+ }
91+ },
92 'default_distro_series': {
93 'default': DISTRO_SERIES.precise,
94 'form': forms.ChoiceField,
95
96=== modified file 'src/maasserver/management/commands/set_up_dns.py'
97--- src/maasserver/management/commands/set_up_dns.py 2013-10-18 16:57:37 +0000
98+++ src/maasserver/management/commands/set_up_dns.py 2013-11-08 06:27:21 +0000
99@@ -26,8 +26,10 @@
100 from optparse import make_option
101
102 from django.core.management.base import BaseCommand
103+from maasserver.models import Config
104 from provisioningserver.dns.config import (
105 DNSConfig,
106+ set_up_options_conf,
107 setup_rndc,
108 )
109
110@@ -49,6 +51,9 @@
111 def handle(self, *args, **options):
112 no_clobber = options.get('no_clobber')
113 setup_rndc()
114+ upstream_dns = Config.objects.get_config("upstream_dns")
115+ set_up_options_conf(
116+ overwrite=not no_clobber, upstream_dns=upstream_dns)
117 config = DNSConfig()
118 config.write_config(
119 overwrite=not no_clobber, zone_names=(), reverse_zone_names=())
120
121=== modified file 'src/maasserver/models/config.py'
122--- src/maasserver/models/config.py 2013-10-18 17:02:17 +0000
123+++ src/maasserver/models/config.py 2013-11-08 06:27:21 +0000
124@@ -56,6 +56,7 @@
125 'default_distro_series': DISTRO_SERIES.precise,
126 'commissioning_distro_series': DISTRO_SERIES.precise,
127 'http_proxy': None,
128+ 'upstream_dns': None,
129 ## /settings
130 }
131
132
133=== modified file 'src/maasserver/tests/test_dns.py'
134--- src/maasserver/tests/test_dns.py 2013-10-18 16:35:07 +0000
135+++ src/maasserver/tests/test_dns.py 2013-11-08 06:27:21 +0000
136@@ -30,7 +30,10 @@
137 NODEGROUP_STATUS,
138 NODEGROUPINTERFACE_MANAGEMENT,
139 )
140-from maasserver.models import node as node_module
141+from maasserver.models import (
142+ Config,
143+ node as node_module,
144+ )
145 from maasserver.testing.factory import factory
146 from maasserver.testing.testcase import MAASServerTestCase
147 from maastesting.bindfixture import BINDServer
148@@ -38,6 +41,7 @@
149 from maastesting.fakemethod import FakeMethod
150 from maastesting.tests.test_bindfixture import dig_call
151 from mock import (
152+ ANY,
153 call,
154 Mock,
155 )
156@@ -285,9 +289,19 @@
157 self.assertEqual(
158 ([(['reload'], True)]), recorder.extract_args())
159
160+ def test_write_full_dns_passes_upstream_dns_parameter(self):
161+ self.patch(settings, 'DNS_CONNECT', True)
162+ self.create_managed_nodegroup()
163+ random_ip = factory.getRandomIPAddress()
164+ Config.objects.set_config("upstream_dns", random_ip)
165+ patched_task = self.patch(dns.tasks.write_full_dns_config, "delay")
166+ dns.write_full_dns_config()
167+ patched_task.assert_called_once_with(
168+ zones=ANY, callback=ANY, upstream_dns=random_ip)
169+
170 def test_write_full_dns_doesnt_call_task_it_no_interface_configured(self):
171 self.patch(settings, 'DNS_CONNECT', True)
172- patched_task = self.patch(tasks, 'write_full_dns_config')
173+ patched_task = self.patch(dns.tasks.write_full_dns_config, "delay")
174 dns.write_full_dns_config()
175 self.assertEqual(0, patched_task.call_count)
176
177
178=== modified file 'src/maastesting/bindfixture.py'
179--- src/maastesting/bindfixture.py 2013-10-18 16:57:37 +0000
180+++ src/maastesting/bindfixture.py 2013-11-08 06:27:21 +0000
181@@ -69,6 +69,8 @@
182 :ivar homedir: A directory where to put all the files the
183 BIND server needs (configuration files and executable).
184 :ivar log_file: The log file allocated for the server.
185+ :ivar include_in_options: Name of a file under homedir to include inside
186+ the options block.
187 """
188
189 # The full path where the 'named' executable can be
190@@ -88,6 +90,9 @@
191 listen-on port {{port}} {127.0.0.1;};
192 pid-file "{{homedir}}/named.pid";
193 session-keyfile "{{homedir}}/session.key";
194+ {{if include_in_options}}
195+ include "{{homedir}}/{{include_in_options}}";
196+ {{endif}}
197 };
198
199 logging{
200@@ -105,13 +110,14 @@
201 """))
202
203 def __init__(self, port=None, rndc_port=None, homedir=None,
204- log_file=None):
205+ log_file=None, include_in_options=None):
206 super(BINDServerResources, self).__init__()
207 self._defaults = dict(
208 port=port,
209 rndc_port=rndc_port,
210 homedir=homedir,
211 log_file=log_file,
212+ include_in_options=include_in_options,
213 )
214
215 def setUp(self, overwrite_config=False):
216@@ -139,6 +145,7 @@
217 self.NAMED_CONF_TEMPLATE.substitute(
218 homedir=self.homedir, port=self.port,
219 log_file=self.log_file,
220+ include_in_options=self.include_in_options,
221 extra=namedrndcconf))
222 atomic_write(
223 GENERATED_HEADER + named_conf, self.conf_file)
224
225=== modified file 'src/maastesting/tests/test_bindfixture.py'
226--- src/maastesting/tests/test_bindfixture.py 2013-10-07 09:12:40 +0000
227+++ src/maastesting/tests/test_bindfixture.py 2013-11-08 06:27:21 +0000
228@@ -27,6 +27,7 @@
229 Contains,
230 FileContains,
231 FileExists,
232+ Not,
233 )
234 from testtools.testcase import gather_details
235
236@@ -90,6 +91,7 @@
237 self.assertIsInstance(resources.rndc_port, int)
238 self.assertIsInstance(resources.homedir, unicode)
239 self.assertIsInstance(resources.log_file, unicode)
240+ self.assertIs(resources.include_in_options, None)
241 self.assertIsInstance(resources.named_file, unicode)
242 self.assertIsInstance(resources.conf_file, unicode)
243 self.assertIsInstance(
244@@ -110,6 +112,23 @@
245 FileContains(matcher=Contains(
246 b'default-port %s' % (
247 resources.rndc_port))))
248+ # This should ideally be in its own test but it's here to cut
249+ # test run time. See test_setUp_honours_include_in_options()
250+ # as its counterpart.
251+ self.assertThat(
252+ resources.conf_file,
253+ Not(FileContains(matcher=Contains(
254+ "forwarders"))))
255+
256+ def test_setUp_honours_include_in_options(self):
257+ forwarders = "forwarders { 1.2.3.4; };"
258+ with BINDServerResources(include_in_options=forwarders) as resources:
259+ expected_in_file = (
260+ resources.homedir + '/' + forwarders).encode("ascii")
261+ self.assertThat(
262+ resources.conf_file,
263+ FileContains(matcher=Contains(
264+ expected_in_file)))
265
266 def test_defaults_reallocated_after_teardown(self):
267 seen_homedirs = set()
268
269=== modified file 'src/provisioningserver/dns/config.py'
270--- src/provisioningserver/dns/config.py 2013-10-15 11:35:10 +0000
271+++ src/provisioningserver/dns/config.py 2013-11-08 06:27:21 +0000
272@@ -17,6 +17,7 @@
273 'DNSForwardZoneConfig',
274 'DNSReverseZoneConfig',
275 'setup_rndc',
276+ 'set_up_options_conf',
277 ]
278
279
280@@ -47,6 +48,7 @@
281
282
283 MAAS_NAMED_CONF_NAME = 'named.conf.maas'
284+MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME = 'named.conf.options.inside.maas'
285 MAAS_NAMED_RNDC_CONF_NAME = 'named.conf.rndc.maas'
286 MAAS_RNDC_CONF_NAME = 'rndc.conf.maas'
287
288@@ -154,6 +156,35 @@
289 TEMPLATES_DIR = 'templates/dns'
290
291
292+def set_up_options_conf(overwrite=True, **kwargs):
293+ """Write out the named.conf.options.inside.maas file.
294+
295+ This file should be included by the top-level named.conf.options
296+ inside its 'options' block. MAAS cannot write the options file itself,
297+ so relies on either the DNSFixture in the test suite, or the packaging.
298+ Both should set that file up appropriately to include our file.
299+ """
300+ template_path = os.path.join(
301+ locate_config(TEMPLATES_DIR),
302+ "named.conf.options.inside.maas.template")
303+ template = tempita.Template.from_filename(template_path)
304+
305+ # Make sure "upstream_dns" is set at least to None. It's a
306+ # special piece of config that can't be obtained in celery
307+ # task code and we don't want to require that every call site
308+ # has to specify it. If it's not set, the substitution will
309+ # fail with the default template that uses this value.
310+ kwargs.setdefault("upstream_dns")
311+ try:
312+ rendered = template.substitute(kwargs)
313+ except NameError as error:
314+ raise DNSConfigFail(*error.args)
315+
316+ target_path = os.path.join(
317+ conf.DNS_CONFIG_DIR, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
318+ atomic_write(rendered, target_path, overwrite=overwrite, mode=0644)
319+
320+
321 class DNSConfigBase:
322 __metaclass__ = ABCMeta
323
324@@ -183,6 +214,7 @@
325 return tempita.Template(f.read(), name=self.template_path)
326
327 def render_template(self, template, **kwargs):
328+ """Substitute supplied kwargs into the supplied Tempita template."""
329 try:
330 return template.substitute(kwargs)
331 except NameError as error:
332
333=== modified file 'src/provisioningserver/dns/tests/test_config.py'
334--- src/provisioningserver/dns/tests/test_config.py 2013-10-15 11:51:48 +0000
335+++ src/provisioningserver/dns/tests/test_config.py 2013-11-08 06:27:21 +0000
336@@ -48,8 +48,10 @@
337 extract_suggested_named_conf,
338 generate_rndc,
339 MAAS_NAMED_CONF_NAME,
340+ MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME,
341 MAAS_NAMED_RNDC_CONF_NAME,
342 MAAS_RNDC_CONF_NAME,
343+ set_up_options_conf,
344 setup_rndc,
345 shortened_reversed_ip,
346 TEMPLATES_DIR,
347@@ -93,6 +95,33 @@
348 conf_content = stream.read()
349 self.assertIn(content, conf_content)
350
351+ def test_set_up_options_conf_writes_configuration(self):
352+ dns_conf_dir = self.make_dir()
353+ self.patch(conf, 'DNS_CONFIG_DIR', dns_conf_dir)
354+ fake_dns = factory.getRandomIPAddress()
355+ set_up_options_conf(upstream_dns=fake_dns)
356+ target_file = os.path.join(
357+ dns_conf_dir, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
358+ self.assertThat(
359+ target_file,
360+ FileContains(matcher=Contains(fake_dns)))
361+
362+ def test_set_up_options_conf_handles_no_upstream_dns(self):
363+ dns_conf_dir = self.make_dir()
364+ self.patch(conf, 'DNS_CONFIG_DIR', dns_conf_dir)
365+ set_up_options_conf()
366+ target_file = os.path.join(
367+ dns_conf_dir, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
368+ self.assertThat(target_file, FileExists())
369+
370+ def test_set_up_options_conf_raises_on_bad_template(self):
371+ template = self.make_file(
372+ name="named.conf.options.inside.maas.template",
373+ contents=b"{{nonexistent}}")
374+ self.patch(config, "TEMPLATES_DIR", os.path.dirname(template))
375+ exception = self.assertRaises(DNSConfigFail, set_up_options_conf)
376+ self.assertIn("name 'nonexistent' is not defined", repr(exception))
377+
378 def test_rndc_config_includes_default_controls(self):
379 dns_conf_dir = self.make_dir()
380 self.patch(conf, 'DNS_CONFIG_DIR', dns_conf_dir)
381
382=== modified file 'src/provisioningserver/tasks.py'
383--- src/provisioningserver/tasks.py 2013-10-15 11:51:48 +0000
384+++ src/provisioningserver/tasks.py 2013-11-08 06:27:21 +0000
385@@ -43,6 +43,7 @@
386 from provisioningserver.dns.config import (
387 DNSConfig,
388 execute_rndc_command,
389+ set_up_options_conf,
390 setup_rndc,
391 )
392 from provisioningserver.omshell import Omshell
393@@ -197,6 +198,8 @@
394 # Write main config file.
395 dns_config = DNSConfig(zones=zones)
396 dns_config.write_config(**kwargs)
397+ # Write the included options file.
398+ set_up_options_conf(**kwargs)
399 if callback is not None:
400 callback.delay()
401
402
403=== modified file 'src/provisioningserver/tests/test_tasks.py'
404--- src/provisioningserver/tests/test_tasks.py 2013-10-15 11:51:48 +0000
405+++ src/provisioningserver/tests/test_tasks.py 2013-11-08 06:27:21 +0000
406@@ -57,6 +57,7 @@
407 DNSForwardZoneConfig,
408 DNSReverseZoneConfig,
409 MAAS_NAMED_CONF_NAME,
410+ MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME,
411 MAAS_NAMED_RNDC_CONF_NAME,
412 MAAS_RNDC_CONF_NAME,
413 )
414@@ -450,7 +451,8 @@
415 command = factory.getRandomString()
416 result = write_full_dns_config.delay(
417 zones=zones,
418- callback=rndc_command.subtask(args=[command]))
419+ callback=rndc_command.subtask(args=[command]),
420+ upstream_dns=factory.getRandomIPAddress())
421
422 forward_file_name = 'zone.%s' % domain
423 reverse_file_name = 'zone.0.168.192.in-addr.arpa'
424@@ -461,6 +463,8 @@
425 os.path.join(self.dns_conf_dir, forward_file_name),
426 os.path.join(self.dns_conf_dir, reverse_file_name),
427 os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME),
428+ os.path.join(
429+ self.dns_conf_dir, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME),
430 ),
431 MatchesListwise(
432 (
433@@ -469,6 +473,7 @@
434 FileExists(),
435 FileExists(),
436 FileExists(),
437+ FileExists(),
438 )))
439
440 def test_write_full_dns_attached_to_dns_worker_queue(self):