Merge lp:~julian-edwards/maas/ntp-server-dhcp 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: 1731
Proposed branch: lp:~julian-edwards/maas/ntp-server-dhcp
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 263 lines (+73/-2)
11 files modified
etc/maas/templates/dhcp/dhcpd.conf.template (+3/-0)
src/maasserver/dhcp.py (+2/-0)
src/maasserver/dhcp_connect.py (+11/-0)
src/maasserver/forms.py (+1/-0)
src/maasserver/forms_settings.py (+11/-0)
src/maasserver/models/config.py (+1/-0)
src/maasserver/tests/test_dhcp.py (+23/-0)
src/provisioningserver/dhcp/config.py (+2/-0)
src/provisioningserver/dhcp/tests/test_config.py (+9/-0)
src/provisioningserver/dhcp/tests/test_writer.py (+6/-2)
src/provisioningserver/dhcp/writer.py (+4/-0)
To merge this branch: bzr merge lp:~julian-edwards/maas/ntp-server-dhcp
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+194659@code.launchpad.net

Commit message

Add a global config for NTP server, which sets the ntp-servers option on DHCP responses to nodes.

Description of the change

I made the default NTP address ntp.ubuntu.com as a convenience. More to the point, I made it the IP address because the DHCP server config requires an IP not a domain name.

Everything else here is pretty self-explanatory, I hope.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Minor nitpick, otherwise looks good.

[1]

+ "e.g. 91.189.94.4 (ntp.ubuntu.com)")

It's not clear from this what the example is. Should I copy just the IP, or the IP and what's inside the parens, or can I just use what's inside the parens?

I'd say something like: "e.g. for ntp.ubuntu.com: '91.189.94.4'".

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

On 11/11/13 18:25, Graham Binns wrote:
> Review: Approve
>
> Minor nitpick, otherwise looks good.
>
> [1]
>
> + "e.g. 91.189.94.4 (ntp.ubuntu.com)")
>
> It's not clear from this what the example is. Should I copy just the IP, or the IP and what's inside the parens, or can I just use what's inside the parens?
>
> I'd say something like: "e.g. for ntp.ubuntu.com: '91.189.94.4'".

Good call, thanks. I was hand-wringing over that one a bit :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'etc/maas/templates/dhcp/dhcpd.conf.template'
2--- etc/maas/templates/dhcp/dhcpd.conf.template 2013-10-17 20:52:59 +0000
3+++ etc/maas/templates/dhcp/dhcpd.conf.template 2013-11-11 09:25:24 +0000
4@@ -13,6 +13,9 @@
5 option domain-name-servers {{dns_servers}};
6 option domain-name "{{domain_name}}";
7 option routers {{router_ip}};
8+ {{if ntp_server}}
9+ option ntp-servers {{ntp_server}};
10+ {{endif}}
11 range dynamic-bootp {{ip_range_low}} {{ip_range_high}};
12 class "PXE" {
13 match if substring (option vendor-class-identifier, 0, 3) = "PXE";
14
15=== modified file 'src/maasserver/dhcp.py'
16--- src/maasserver/dhcp.py 2013-10-07 09:12:40 +0000
17+++ src/maasserver/dhcp.py 2013-11-11 09:25:24 +0000
18@@ -22,6 +22,7 @@
19 NODEGROUP_STATUS,
20 NODEGROUPINTERFACE_MANAGEMENT,
21 )
22+from maasserver.models import Config
23 from netaddr import IPAddress
24 from provisioningserver.tasks import (
25 restart_dhcp_server,
26@@ -67,6 +68,7 @@
27 broadcast_ip=interface.broadcast_ip,
28 router_ip=interface.router_ip,
29 dns_servers=get_dns_server_address(nodegroup),
30+ ntp_server=Config.objects.get_config("ntp_server"),
31 domain_name=nodegroup.name,
32 ip_range_low=interface.ip_range_low,
33 ip_range_high=interface.ip_range_high,
34
35=== modified file 'src/maasserver/dhcp_connect.py'
36--- src/maasserver/dhcp_connect.py 2013-10-07 09:12:40 +0000
37+++ src/maasserver/dhcp_connect.py 2013-11-11 09:25:24 +0000
38@@ -19,6 +19,7 @@
39 from django.db.models.signals import post_save
40 from django.dispatch import receiver
41 from maasserver.models import (
42+ Config,
43 NodeGroup,
44 NodeGroupInterface,
45 )
46@@ -43,3 +44,13 @@
47
48
49 connect_to_field_change(dhcp_post_edit_status_NodeGroup, NodeGroup, 'status')
50+
51+
52+def ntp_server_changed(sender, instance, created, **kwargs):
53+ """The ntp_server config item changed, so write new DHCP configs."""
54+ from maasserver.dhcp import configure_dhcp
55+ for nodegroup in NodeGroup.objects.all():
56+ configure_dhcp(nodegroup)
57+
58+
59+Config.objects.config_changed_connect("ntp_server", ntp_server_changed)
60
61=== modified file 'src/maasserver/forms.py'
62--- src/maasserver/forms.py 2013-11-05 03:47:04 +0000
63+++ src/maasserver/forms.py 2013-11-11 09:25:24 +0000
64@@ -627,6 +627,7 @@
65 enlistment_domain = get_config_field('enlistment_domain')
66 http_proxy = get_config_field('http_proxy')
67 upstream_dns = get_config_field('upstream_dns')
68+ ntp_server = get_config_field('ntp_server')
69
70
71 class CommissioningForm(ConfigForm):
72
73=== modified file 'src/maasserver/forms_settings.py'
74--- src/maasserver/forms_settings.py 2013-11-08 06:26:55 +0000
75+++ src/maasserver/forms_settings.py 2013-11-11 09:25:24 +0000
76@@ -184,6 +184,17 @@
77 "server config.")
78 }
79 },
80+ 'ntp_server': {
81+ 'default': None,
82+ 'form': forms.GenericIPAddressField,
83+ 'form_kwargs': {
84+ 'label': "Address of NTP server for nodes",
85+ 'required': False,
86+ 'help_text': (
87+ "NTP server address passed to nodes via a DHCP response. "
88+ "e.g. for ntp.ubuntu.com: '91.189.94.4'")
89+ }
90+ },
91 'default_distro_series': {
92 'default': DISTRO_SERIES.precise,
93 'form': forms.ChoiceField,
94
95=== modified file 'src/maasserver/models/config.py'
96--- src/maasserver/models/config.py 2013-11-05 03:47:04 +0000
97+++ src/maasserver/models/config.py 2013-11-11 09:25:24 +0000
98@@ -57,6 +57,7 @@
99 'commissioning_distro_series': DISTRO_SERIES.precise,
100 'http_proxy': None,
101 'upstream_dns': None,
102+ 'ntp_server': '91.189.94.4', # ntp.ubuntu.com
103 ## /settings
104 }
105
106
107=== modified file 'src/maasserver/tests/test_dhcp.py'
108--- src/maasserver/tests/test_dhcp.py 2013-10-15 06:52:13 +0000
109+++ src/maasserver/tests/test_dhcp.py 2013-11-11 09:25:24 +0000
110@@ -15,6 +15,7 @@
111 __all__ = []
112
113 from functools import partial
114+import random
115
116 from django.conf import settings
117 from maasserver import dhcp
118@@ -24,6 +25,8 @@
119 )
120 from maasserver.dns import get_dns_server_address
121 from maasserver.enum import NODEGROUP_STATUS
122+from maasserver.models import Config
123+from maasserver.models.config import get_default_config
124 from maasserver.testing.factory import factory
125 from maasserver.testing.testcase import MAASServerTestCase
126 from maastesting.celery import CeleryFixture
127@@ -84,6 +87,7 @@
128
129 expected_params["omapi_key"] = nodegroup.dhcp_key
130 expected_params["dns_servers"] = get_dns_server_address()
131+ expected_params["ntp_server"] = get_default_config()['ntp_server']
132 expected_params["domain_name"] = nodegroup.name
133 expected_params["subnet"] = '192.168.100.0'
134 expected_params["dhcp_interfaces"] = interface.interface
135@@ -174,3 +178,22 @@
136 dhcp.write_dhcp_config.apply_async.call_count,
137 kwargs['kwargs']['router_ip'],
138 ))
139+
140+ def test_dhcp_config_gets_written_when_ntp_server_changes(self):
141+ # When the "ntp_server" Config item is changed, check that all
142+ # nodegroups get their DHCP config re-written.
143+ num_active_nodegroups = random.randint(1, 10)
144+ num_inactive_nodegroups = random.randint(1, 10)
145+ for x in range(num_active_nodegroups):
146+ factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
147+ for x in range(num_inactive_nodegroups):
148+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING)
149+
150+ self.patch(settings, "DHCP_CONNECT", True)
151+ self.patch(dhcp, 'write_dhcp_config')
152+
153+ Config.objects.set_config("ntp_server", factory.getRandomIPAddress())
154+
155+ self.assertEqual(
156+ num_active_nodegroups,
157+ dhcp.write_dhcp_config.apply_async.call_count)
158
159=== modified file 'src/provisioningserver/dhcp/config.py'
160--- src/provisioningserver/dhcp/config.py 2013-10-07 09:12:40 +0000
161+++ src/provisioningserver/dhcp/config.py 2013-11-11 09:25:24 +0000
162@@ -41,6 +41,7 @@
163 e.g. 192.168.1.255
164 :param dns_servers: One or more IP addresses of the DNS server for the
165 subnet
166+ :param ntp_server: IP address of the NTP server for the nodes
167 :param domain_name: name that will be appended to the client's hostname to
168 form a fully-qualified domain-name
169 :param gateway: The router/gateway IP address for the subnet.
170@@ -52,6 +53,7 @@
171 template_file = locate_config(TEMPLATES_DIR, 'dhcpd.conf.template')
172 params['bootloader'] = compose_bootloader_path()
173 params['platform_codename'] = linux_distribution()[2]
174+ params.setdefault("ntp_server")
175 try:
176 template = tempita.Template.from_filename(
177 template_file, encoding="UTF-8")
178
179=== modified file 'src/provisioningserver/dhcp/tests/test_config.py'
180--- src/provisioningserver/dhcp/tests/test_config.py 2013-10-30 01:21:13 +0000
181+++ src/provisioningserver/dhcp/tests/test_config.py 2013-11-11 09:25:24 +0000
182@@ -55,6 +55,7 @@
183 subnet_mask="255.0.0.0",
184 broadcast_ip="10.255.255.255",
185 dns_servers="10.1.0.1 10.1.0.2",
186+ ntp_server="8.8.8.8",
187 domain_name="example.com",
188 router_ip="10.0.0.2",
189 ip_range_low="10.0.0.3",
190@@ -109,3 +110,11 @@
191 params = make_sample_params()
192 output = config.get_config(**params)
193 self.assertThat(output, Contains(compose_bootloader_path()))
194+
195+ def test_renders_without_ntp_servers_set(self):
196+ params = make_sample_params()
197+ del params['ntp_server']
198+ template = self.patch_template()
199+ rendered = template.substitute(params)
200+ self.assertEqual(rendered, config.get_config(**params))
201+ self.assertNotIn("ntp-servers", rendered)
202
203=== modified file 'src/provisioningserver/dhcp/tests/test_writer.py'
204--- src/provisioningserver/dhcp/tests/test_writer.py 2013-10-07 09:12:40 +0000
205+++ src/provisioningserver/dhcp/tests/test_writer.py 2013-11-11 09:25:24 +0000
206@@ -38,6 +38,7 @@
207 '--subnet-mask', 'subnet-mask',
208 '--broadcast-ip', 'broadcast-ip',
209 '--dns-servers', 'dns-servers',
210+ '--ntp-server', 'ntp-server',
211 '--domain-name', 'domain-name',
212 '--router-ip', 'router-ip',
213 '--ip-range-low', 'ip-range-low',
214@@ -53,8 +54,8 @@
215 output, err = cmd.communicate()
216 contains_all_params = ContainsAll(
217 ['subnet', 'subnet-mask', 'broadcast-ip',
218- 'omapi-key', 'dns-servers', 'domain-name', 'router-ip',
219- 'ip-range-low', 'ip-range-high'])
220+ 'omapi-key', 'dns-servers', 'ntp-server', 'domain-name',
221+ 'router-ip', 'ip-range-low', 'ip-range-high'])
222 self.assertThat(output, contains_all_params)
223
224 def test_arg_setup(self):
225@@ -67,6 +68,7 @@
226 subnet_mask='subnet-mask',
227 broadcast_ip='broadcast-ip',
228 dns_servers='dns-servers',
229+ ntp_server='ntp-server',
230 domain_name='domain-name',
231 router_ip='router-ip',
232 omapi_key='omapi-key',
233@@ -86,6 +88,7 @@
234 'broadcast-ip',
235 'omapi-key',
236 'dns-servers',
237+ 'ntp-server',
238 'domain-name',
239 'router-ip',
240 'ip-range-low',
241@@ -108,6 +111,7 @@
242 'broadcast-ip',
243 'omapi-key',
244 'dns-servers',
245+ 'ntp-server',
246 'domain-name',
247 'router-ip',
248 'ip-range-low',
249
250=== modified file 'src/provisioningserver/dhcp/writer.py'
251--- src/provisioningserver/dhcp/writer.py 2013-10-15 11:35:10 +0000
252+++ src/provisioningserver/dhcp/writer.py 2013-11-11 09:25:24 +0000
253@@ -41,6 +41,10 @@
254 "One or more IP addresses of the DNS server for the subnet "
255 "separated by spaces."))
256 parser.add_argument(
257+ "--ntp-server", action="store", required=True, help=(
258+ "IP address of the NTP server to pass to nodes in the DHCP "
259+ "response."))
260+ parser.add_argument(
261 "--domain-name", action="store", required=True, help=(
262 "The domain name that will be appended to the client's hostname "
263 "to form a fully-qualified domain-name (FQDN)."))