Merge lp:~jtv/maas/bug-1299374 into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 2206
Proposed branch: lp:~jtv/maas/bug-1299374
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 303 lines (+75/-58)
4 files modified
src/maasserver/models/nodegroup.py (+2/-2)
src/maasserver/models/nodegroupinterface.py (+29/-30)
src/maasserver/models/tests/test_nodegroupinterface.py (+42/-25)
src/maasserver/rpc/tests/test_regionservice.py (+2/-1)
To merge this branch: bzr merge lp:~jtv/maas/bug-1299374
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+213367@code.launchpad.net

Commit message

Make NodeGroupInterface.broadcast_ip optional; default to the network's default netmask, as a user might expect. Derive network from interface IP address and netmask, not broadcast address and netmask.

Description of the change

When I had to re-install my testing MAAS today, I was forced to enter a broadcast IP for a managed cluster interface. That's just silly. With perhaps some strange exceptions that sysadmins would know about, a network's broadcast address is simply the last IP address in the network's range — the one that has all host bits set to one. You're never forced to enter it anywhere because given a network's address and size, the standard value is obvious.

Nor did it make much sense to derive the network information from the interface's broadcast address. The interface's own network address has to be on the same network, and is required, so we might as well use that. There is nothing special about the broadcast address in this regard.

There was one strange thing with this branch. When I ran tests, the Selenium tests failed, as they always do on this system. But in addition to this, I got a failure in TestRegionService: test_stopping_logs_errors_when_closing_connections failed its assertDocTestMatches check, even though as far as I can tell the output does match. No discernible connection to my changes whatsoever.

It looks like a test isolation error. All test cases in test_regionservice.py are MAASTestCase, but they do modify the database — so you'd expect that they would have to be MAASServerTestCase. Turning them all into MAASServerTestCase broke more tests, but promoting just TestRegionService to MAASServerTestCase stopped the problem.

Jeroen

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Nice!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/nodegroup.py'
2--- src/maasserver/models/nodegroup.py 2014-03-27 17:52:53 +0000
3+++ src/maasserver/models/nodegroup.py 2014-03-29 17:30:47 +0000
4@@ -68,13 +68,13 @@
5 dhcp_values = [
6 interface,
7 subnet_mask,
8- broadcast_ip,
9 router_ip,
10 ip_range_low,
11 ip_range_high,
12 ]
13 assert all(dhcp_values) or not any(dhcp_values), (
14- "Provide all DHCP settings, or none at all.")
15+ "Provide all DHCP settings, or none at all. "
16+ "Only the broadcast address is optional.")
17
18 if cluster_name is None:
19 cluster_name = NODEGROUP_CLUSTER_NAME_TEMPLATE % {'uuid': uuid}
20
21=== modified file 'src/maasserver/models/nodegroupinterface.py'
22--- src/maasserver/models/nodegroupinterface.py 2014-02-07 04:10:09 +0000
23+++ src/maasserver/models/nodegroupinterface.py 2014-03-29 17:30:47 +0000
24@@ -86,22 +86,19 @@
25
26 @property
27 def network(self):
28- """Return the network defined by the broadcast address and net mask.
29-
30- If either the broadcast address or the subnet mask is unset, returns
31- None.
32-
33- :return: :class:`IPNetwork`
34- :raise AddrFormatError: If the combination of broadcast address and
35+ """Return the network defined by the interface's address and netmask.
36+
37+ :return: :class:`IPNetwork`, or `None` if the netmask is unset.
38+ :raise AddrFormatError: If the combination of interface address and
39 subnet mask is malformed.
40 """
41- broadcast = self.broadcast_ip
42+ ip = self.ip
43 netmask = self.subnet_mask
44 # Nullness check for GenericIPAddress fields is deliberately kept
45 # vague: GenericIPAddressField seems to represent nulls as empty
46 # strings.
47- if broadcast and netmask:
48- return make_network(broadcast, netmask).cidr
49+ if netmask:
50+ return make_network(ip, netmask).cidr
51 else:
52 return None
53
54@@ -116,20 +113,17 @@
55 def clean_network_valid(self):
56 """Validate the network.
57
58- This validates that the network defined by broadcast_ip and
59- subnet_mask is valid.
60+ This validates that the network defined by `ip` and `subnet_mask` is
61+ valid.
62 """
63 try:
64 self.network
65- except AddrFormatError, e:
66- # Technically, this should be a global error but it's
67- # more user-friendly to precisely point out where the error
68- # comes from.
69- raise ValidationError(
70- {
71- 'broadcast_ip': [e.message],
72- 'subnet_mask': [e.message],
73- })
74+ except AddrFormatError as e:
75+ # The interface's address is validated separately. If the
76+ # combination with the netmask is invalid, either there's already
77+ # going to be a specific validation error for the IP address, or
78+ # the failure is due to an invalid netmask.
79+ raise ValidationError({'subnet_mask': [e.message]})
80
81 def clean_network_not_too_big(self):
82 # If management is not 'UNMANAGED', refuse huge networks.
83@@ -141,10 +135,7 @@
84 "Cannot create an address space bigger than "
85 "a /%d network. This network is a /%d network." % (
86 MINIMUM_NETMASK_BITS, network.prefixlen))
87- raise ValidationError({
88- 'broadcast_ip': [message],
89- 'subnet_mask': [message],
90- })
91+ raise ValidationError({'subnet_mask': [message]})
92
93 def clean_network_config_if_managed(self):
94 # If management is not 'UNMANAGED', all the network information
95@@ -152,7 +143,6 @@
96 if self.management != NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED:
97 mandatory_fields = [
98 'interface',
99- 'broadcast_ip',
100 'subnet_mask',
101 'router_ip',
102 'ip_range_low',
103@@ -170,15 +160,18 @@
104 def clean_ips_in_network(self):
105 """Ensure that the network settings are all congruent.
106
107- Specifically, it ensures that the interface address, router address,
108- and the address range, all fall within the network defined by the
109- broadcast address and subnet mask.
110+ Specifically, it ensures that the router address, the DHCP address
111+ range, and the broadcast address if given, all fall within the network
112+ defined by the interface's IP address and the subnet mask.
113+
114+ If no broadcast address is given, the network's default broadcast
115+ address will be used.
116 """
117 network = self.network
118 if network is None:
119 return
120 network_settings = (
121- ("ip", self.ip),
122+ ("broadcast_ip", self.broadcast_ip),
123 ("router_ip", self.router_ip),
124 ("ip_range_low", self.ip_range_low),
125 ("ip_range_high", self.ip_range_high),
126@@ -191,6 +184,12 @@
127 if len(network_errors) != 0:
128 raise ValidationError(network_errors)
129
130+ # Deliberately vague nullness check. A null IP address seems to be
131+ # None in some situations, or an empty string in others.
132+ if not self.broadcast_ip:
133+ # No broadcast address given. Set the default.
134+ self.broadcast_ip = network.broadcast
135+
136 def clean_fields(self, *args, **kwargs):
137 super(NodeGroupInterface, self).clean_fields(*args, **kwargs)
138 self.clean_network_valid()
139
140=== modified file 'src/maasserver/models/tests/test_nodegroupinterface.py'
141--- src/maasserver/models/tests/test_nodegroupinterface.py 2014-02-07 04:10:09 +0000
142+++ src/maasserver/models/tests/test_nodegroupinterface.py 2014-03-29 17:30:47 +0000
143@@ -20,7 +20,10 @@
144 NODEGROUPINTERFACE_MANAGEMENT,
145 NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT,
146 )
147-from maasserver.models import NodeGroup
148+from maasserver.models import (
149+ NodeGroup,
150+ NodeGroupInterface,
151+ )
152 from maasserver.models.nodegroupinterface import MINIMUM_NETMASK_BITS
153 from maasserver.testing.factory import factory
154 from maasserver.testing.testcase import MAASServerTestCase
155@@ -44,28 +47,28 @@
156 interface = make_interface(network=network)
157 self.assertEqual(IPNetwork("10.0.0.0/24"), interface.network)
158
159- def test_network_is_defined_when_broadcast_and_mask_are(self):
160+ def test_network_is_defined_when_netmask_is(self):
161 interface = make_interface()
162- interface.broadcast = "10.0.0.255"
163+ interface.ip = "10.0.0.9"
164 interface.subnet_mask = "255.255.255.0"
165 self.assertIsInstance(interface.network, IPNetwork)
166
167- def test_network_is_undefined_when_broadcast_is_None(self):
168+ def test_network_does_not_require_broadcast_address(self):
169 interface = make_interface()
170 interface.broadcast_ip = None
171- self.assertIsNone(interface.network)
172+ self.assertIsInstance(interface.network, IPNetwork)
173
174- def test_network_is_undefined_when_broadcast_is_empty(self):
175+ def test_network_does_not_require_nonempty_broadcast_address(self):
176 interface = make_interface()
177 interface.broadcast_ip = ""
178- self.assertIsNone(interface.network)
179+ self.assertIsInstance(interface.network, IPNetwork)
180
181 def test_network_is_undefined_when_subnet_mask_is_None(self):
182 interface = make_interface()
183 interface.subnet_mask = None
184 self.assertIsNone(interface.network)
185
186- def test_network_is_undefined_subnet_mask_is_empty(self):
187+ def test_network_is_undefined_when_subnet_mask_is_empty(self):
188 interface = make_interface()
189 interface.subnet_mask = ""
190 self.assertIsNone(interface.network)
191@@ -78,8 +81,9 @@
192
193 def test_clean_ips_in_network_validates_IP(self):
194 network = IPNetwork('192.168.0.3/24')
195+ ip_outside_network = '192.168.2.1'
196 checked_fields = [
197- 'ip',
198+ 'broadcast_ip',
199 'router_ip',
200 'ip_range_low',
201 'ip_range_high',
202@@ -87,14 +91,14 @@
203 for field in checked_fields:
204 nodegroup = factory.make_node_group(network=network)
205 [interface] = nodegroup.get_managed_interfaces()
206- ip = '192.168.2.1'
207- setattr(interface, field, '192.168.2.1')
208- message = (
209- "%s not in the %s network" % (ip, '192.168.0.0/24'))
210+ setattr(interface, field, ip_outside_network)
211+ message = "%s not in the %s network" % (
212+ ip_outside_network,
213+ '192.168.0.0/24',
214+ )
215 exception = self.assertRaises(
216 ValidationError, interface.full_clean)
217- self.assertEqual(
218- {field: [message]}, exception.message_dict)
219+ self.assertEqual({field: [message]}, exception.message_dict)
220
221 def test_clean_network(self):
222 nodegroup = factory.make_node_group(
223@@ -102,13 +106,10 @@
224 [interface] = nodegroup.get_managed_interfaces()
225 # Set a bogus subnet mask.
226 interface.subnet_mask = '0.9.0.4'
227- message = 'invalid IPNetwork 192.168.0.255/0.9.0.4'
228+ message = "invalid IPNetwork %s/0.9.0.4" % interface.ip
229 exception = self.assertRaises(ValidationError, interface.full_clean)
230 self.assertEqual(
231- {
232- 'subnet_mask': [message],
233- 'broadcast_ip': [message],
234- },
235+ {'subnet_mask': [message]},
236 exception.message_dict)
237
238 def test_clean_network_rejects_huge_network(self):
239@@ -120,10 +121,7 @@
240 "This network is a /%d network." % (
241 MINIMUM_NETMASK_BITS, MINIMUM_NETMASK_BITS - 1))
242 self.assertEqual(
243- {
244- 'subnet_mask': [message],
245- 'broadcast_ip': [message],
246- },
247+ {'subnet_mask': [message]},
248 exception.message_dict)
249
250 def test_clean_network_accepts_network_if_not_too_big(self):
251@@ -142,7 +140,6 @@
252 network = IPNetwork('192.168.0.3/24')
253 checked_fields = [
254 'interface',
255- 'broadcast_ip',
256 'subnet_mask',
257 'router_ip',
258 'ip_range_low',
259@@ -160,3 +157,23 @@
260 "That field cannot be empty (unless that interface is "
261 "'unmanaged')")
262 self.assertEqual({field: [message]}, exception.message_dict)
263+
264+ def test_clean_network_config_sets_default_if_netmask_not_given(self):
265+ network = factory.getRandomNetwork()
266+ nodegroup = factory.make_node_group(
267+ network=network,
268+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
269+ [interface] = nodegroup.get_managed_interfaces()
270+ interface.full_clean()
271+ self.assertEqual(unicode(network.broadcast), interface.broadcast_ip)
272+
273+ def test_clean_network_config_sets_no_broadcast_without_netmask(self):
274+ network = factory.getRandomNetwork()
275+ nodegroup = factory.make_node_group(
276+ network=network,
277+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
278+ interface = NodeGroupInterface.objects.get(nodegroup=nodegroup)
279+ interface.subnet_mask = None
280+ interface.broadcast_ip = None
281+ interface.full_clean()
282+ self.assertIsNone(interface.broadcast_ip)
283
284=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
285--- src/maasserver/rpc/tests/test_regionservice.py 2014-03-28 03:55:55 +0000
286+++ src/maasserver/rpc/tests/test_regionservice.py 2014-03-29 17:30:47 +0000
287@@ -34,6 +34,7 @@
288 RegionService,
289 )
290 from maasserver.rpc.testing.doubles import IdentifyingRegionServer
291+from maasserver.testing.testcase import MAASServerTestCase
292 from maastesting.factory import factory
293 from maastesting.matchers import (
294 MockCalledOnceWith,
295@@ -195,7 +196,7 @@
296 from zope.interface.verify import verifyObject
297
298
299-class TestRegionServer(MAASTestCase):
300+class TestRegionServer(MAASServerTestCase):
301
302 def test_interfaces(self):
303 protocol = RegionServer()