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
=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py 2014-03-27 17:52:53 +0000
+++ src/maasserver/models/nodegroup.py 2014-03-29 17:30:47 +0000
@@ -68,13 +68,13 @@
68 dhcp_values = [68 dhcp_values = [
69 interface,69 interface,
70 subnet_mask,70 subnet_mask,
71 broadcast_ip,
72 router_ip,71 router_ip,
73 ip_range_low,72 ip_range_low,
74 ip_range_high,73 ip_range_high,
75 ]74 ]
76 assert all(dhcp_values) or not any(dhcp_values), (75 assert all(dhcp_values) or not any(dhcp_values), (
77 "Provide all DHCP settings, or none at all.")76 "Provide all DHCP settings, or none at all. "
77 "Only the broadcast address is optional.")
7878
79 if cluster_name is None:79 if cluster_name is None:
80 cluster_name = NODEGROUP_CLUSTER_NAME_TEMPLATE % {'uuid': uuid}80 cluster_name = NODEGROUP_CLUSTER_NAME_TEMPLATE % {'uuid': uuid}
8181
=== modified file 'src/maasserver/models/nodegroupinterface.py'
--- src/maasserver/models/nodegroupinterface.py 2014-02-07 04:10:09 +0000
+++ src/maasserver/models/nodegroupinterface.py 2014-03-29 17:30:47 +0000
@@ -86,22 +86,19 @@
8686
87 @property87 @property
88 def network(self):88 def network(self):
89 """Return the network defined by the broadcast address and net mask.89 """Return the network defined by the interface's address and netmask.
9090
91 If either the broadcast address or the subnet mask is unset, returns91 :return: :class:`IPNetwork`, or `None` if the netmask is unset.
92 None.92 :raise AddrFormatError: If the combination of interface address and
93
94 :return: :class:`IPNetwork`
95 :raise AddrFormatError: If the combination of broadcast address and
96 subnet mask is malformed.93 subnet mask is malformed.
97 """94 """
98 broadcast = self.broadcast_ip95 ip = self.ip
99 netmask = self.subnet_mask96 netmask = self.subnet_mask
100 # Nullness check for GenericIPAddress fields is deliberately kept97 # Nullness check for GenericIPAddress fields is deliberately kept
101 # vague: GenericIPAddressField seems to represent nulls as empty98 # vague: GenericIPAddressField seems to represent nulls as empty
102 # strings.99 # strings.
103 if broadcast and netmask:100 if netmask:
104 return make_network(broadcast, netmask).cidr101 return make_network(ip, netmask).cidr
105 else:102 else:
106 return None103 return None
107104
@@ -116,20 +113,17 @@
116 def clean_network_valid(self):113 def clean_network_valid(self):
117 """Validate the network.114 """Validate the network.
118115
119 This validates that the network defined by broadcast_ip and116 This validates that the network defined by `ip` and `subnet_mask` is
120 subnet_mask is valid.117 valid.
121 """118 """
122 try:119 try:
123 self.network120 self.network
124 except AddrFormatError, e:121 except AddrFormatError as e:
125 # Technically, this should be a global error but it's122 # The interface's address is validated separately. If the
126 # more user-friendly to precisely point out where the error123 # combination with the netmask is invalid, either there's already
127 # comes from.124 # going to be a specific validation error for the IP address, or
128 raise ValidationError(125 # the failure is due to an invalid netmask.
129 {126 raise ValidationError({'subnet_mask': [e.message]})
130 'broadcast_ip': [e.message],
131 'subnet_mask': [e.message],
132 })
133127
134 def clean_network_not_too_big(self):128 def clean_network_not_too_big(self):
135 # If management is not 'UNMANAGED', refuse huge networks.129 # If management is not 'UNMANAGED', refuse huge networks.
@@ -141,10 +135,7 @@
141 "Cannot create an address space bigger than "135 "Cannot create an address space bigger than "
142 "a /%d network. This network is a /%d network." % (136 "a /%d network. This network is a /%d network." % (
143 MINIMUM_NETMASK_BITS, network.prefixlen))137 MINIMUM_NETMASK_BITS, network.prefixlen))
144 raise ValidationError({138 raise ValidationError({'subnet_mask': [message]})
145 'broadcast_ip': [message],
146 'subnet_mask': [message],
147 })
148139
149 def clean_network_config_if_managed(self):140 def clean_network_config_if_managed(self):
150 # If management is not 'UNMANAGED', all the network information141 # If management is not 'UNMANAGED', all the network information
@@ -152,7 +143,6 @@
152 if self.management != NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED:143 if self.management != NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED:
153 mandatory_fields = [144 mandatory_fields = [
154 'interface',145 'interface',
155 'broadcast_ip',
156 'subnet_mask',146 'subnet_mask',
157 'router_ip',147 'router_ip',
158 'ip_range_low',148 'ip_range_low',
@@ -170,15 +160,18 @@
170 def clean_ips_in_network(self):160 def clean_ips_in_network(self):
171 """Ensure that the network settings are all congruent.161 """Ensure that the network settings are all congruent.
172162
173 Specifically, it ensures that the interface address, router address,163 Specifically, it ensures that the router address, the DHCP address
174 and the address range, all fall within the network defined by the164 range, and the broadcast address if given, all fall within the network
175 broadcast address and subnet mask.165 defined by the interface's IP address and the subnet mask.
166
167 If no broadcast address is given, the network's default broadcast
168 address will be used.
176 """169 """
177 network = self.network170 network = self.network
178 if network is None:171 if network is None:
179 return172 return
180 network_settings = (173 network_settings = (
181 ("ip", self.ip),174 ("broadcast_ip", self.broadcast_ip),
182 ("router_ip", self.router_ip),175 ("router_ip", self.router_ip),
183 ("ip_range_low", self.ip_range_low),176 ("ip_range_low", self.ip_range_low),
184 ("ip_range_high", self.ip_range_high),177 ("ip_range_high", self.ip_range_high),
@@ -191,6 +184,12 @@
191 if len(network_errors) != 0:184 if len(network_errors) != 0:
192 raise ValidationError(network_errors)185 raise ValidationError(network_errors)
193186
187 # Deliberately vague nullness check. A null IP address seems to be
188 # None in some situations, or an empty string in others.
189 if not self.broadcast_ip:
190 # No broadcast address given. Set the default.
191 self.broadcast_ip = network.broadcast
192
194 def clean_fields(self, *args, **kwargs):193 def clean_fields(self, *args, **kwargs):
195 super(NodeGroupInterface, self).clean_fields(*args, **kwargs)194 super(NodeGroupInterface, self).clean_fields(*args, **kwargs)
196 self.clean_network_valid()195 self.clean_network_valid()
197196
=== modified file 'src/maasserver/models/tests/test_nodegroupinterface.py'
--- src/maasserver/models/tests/test_nodegroupinterface.py 2014-02-07 04:10:09 +0000
+++ src/maasserver/models/tests/test_nodegroupinterface.py 2014-03-29 17:30:47 +0000
@@ -20,7 +20,10 @@
20 NODEGROUPINTERFACE_MANAGEMENT,20 NODEGROUPINTERFACE_MANAGEMENT,
21 NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT,21 NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT,
22 )22 )
23from maasserver.models import NodeGroup23from maasserver.models import (
24 NodeGroup,
25 NodeGroupInterface,
26 )
24from maasserver.models.nodegroupinterface import MINIMUM_NETMASK_BITS27from maasserver.models.nodegroupinterface import MINIMUM_NETMASK_BITS
25from maasserver.testing.factory import factory28from maasserver.testing.factory import factory
26from maasserver.testing.testcase import MAASServerTestCase29from maasserver.testing.testcase import MAASServerTestCase
@@ -44,28 +47,28 @@
44 interface = make_interface(network=network)47 interface = make_interface(network=network)
45 self.assertEqual(IPNetwork("10.0.0.0/24"), interface.network)48 self.assertEqual(IPNetwork("10.0.0.0/24"), interface.network)
4649
47 def test_network_is_defined_when_broadcast_and_mask_are(self):50 def test_network_is_defined_when_netmask_is(self):
48 interface = make_interface()51 interface = make_interface()
49 interface.broadcast = "10.0.0.255"52 interface.ip = "10.0.0.9"
50 interface.subnet_mask = "255.255.255.0"53 interface.subnet_mask = "255.255.255.0"
51 self.assertIsInstance(interface.network, IPNetwork)54 self.assertIsInstance(interface.network, IPNetwork)
5255
53 def test_network_is_undefined_when_broadcast_is_None(self):56 def test_network_does_not_require_broadcast_address(self):
54 interface = make_interface()57 interface = make_interface()
55 interface.broadcast_ip = None58 interface.broadcast_ip = None
56 self.assertIsNone(interface.network)59 self.assertIsInstance(interface.network, IPNetwork)
5760
58 def test_network_is_undefined_when_broadcast_is_empty(self):61 def test_network_does_not_require_nonempty_broadcast_address(self):
59 interface = make_interface()62 interface = make_interface()
60 interface.broadcast_ip = ""63 interface.broadcast_ip = ""
61 self.assertIsNone(interface.network)64 self.assertIsInstance(interface.network, IPNetwork)
6265
63 def test_network_is_undefined_when_subnet_mask_is_None(self):66 def test_network_is_undefined_when_subnet_mask_is_None(self):
64 interface = make_interface()67 interface = make_interface()
65 interface.subnet_mask = None68 interface.subnet_mask = None
66 self.assertIsNone(interface.network)69 self.assertIsNone(interface.network)
6770
68 def test_network_is_undefined_subnet_mask_is_empty(self):71 def test_network_is_undefined_when_subnet_mask_is_empty(self):
69 interface = make_interface()72 interface = make_interface()
70 interface.subnet_mask = ""73 interface.subnet_mask = ""
71 self.assertIsNone(interface.network)74 self.assertIsNone(interface.network)
@@ -78,8 +81,9 @@
7881
79 def test_clean_ips_in_network_validates_IP(self):82 def test_clean_ips_in_network_validates_IP(self):
80 network = IPNetwork('192.168.0.3/24')83 network = IPNetwork('192.168.0.3/24')
84 ip_outside_network = '192.168.2.1'
81 checked_fields = [85 checked_fields = [
82 'ip',86 'broadcast_ip',
83 'router_ip',87 'router_ip',
84 'ip_range_low',88 'ip_range_low',
85 'ip_range_high',89 'ip_range_high',
@@ -87,14 +91,14 @@
87 for field in checked_fields:91 for field in checked_fields:
88 nodegroup = factory.make_node_group(network=network)92 nodegroup = factory.make_node_group(network=network)
89 [interface] = nodegroup.get_managed_interfaces()93 [interface] = nodegroup.get_managed_interfaces()
90 ip = '192.168.2.1'94 setattr(interface, field, ip_outside_network)
91 setattr(interface, field, '192.168.2.1')95 message = "%s not in the %s network" % (
92 message = (96 ip_outside_network,
93 "%s not in the %s network" % (ip, '192.168.0.0/24'))97 '192.168.0.0/24',
98 )
94 exception = self.assertRaises(99 exception = self.assertRaises(
95 ValidationError, interface.full_clean)100 ValidationError, interface.full_clean)
96 self.assertEqual(101 self.assertEqual({field: [message]}, exception.message_dict)
97 {field: [message]}, exception.message_dict)
98102
99 def test_clean_network(self):103 def test_clean_network(self):
100 nodegroup = factory.make_node_group(104 nodegroup = factory.make_node_group(
@@ -102,13 +106,10 @@
102 [interface] = nodegroup.get_managed_interfaces()106 [interface] = nodegroup.get_managed_interfaces()
103 # Set a bogus subnet mask.107 # Set a bogus subnet mask.
104 interface.subnet_mask = '0.9.0.4'108 interface.subnet_mask = '0.9.0.4'
105 message = 'invalid IPNetwork 192.168.0.255/0.9.0.4'109 message = "invalid IPNetwork %s/0.9.0.4" % interface.ip
106 exception = self.assertRaises(ValidationError, interface.full_clean)110 exception = self.assertRaises(ValidationError, interface.full_clean)
107 self.assertEqual(111 self.assertEqual(
108 {112 {'subnet_mask': [message]},
109 'subnet_mask': [message],
110 'broadcast_ip': [message],
111 },
112 exception.message_dict)113 exception.message_dict)
113114
114 def test_clean_network_rejects_huge_network(self):115 def test_clean_network_rejects_huge_network(self):
@@ -120,10 +121,7 @@
120 "This network is a /%d network." % (121 "This network is a /%d network." % (
121 MINIMUM_NETMASK_BITS, MINIMUM_NETMASK_BITS - 1))122 MINIMUM_NETMASK_BITS, MINIMUM_NETMASK_BITS - 1))
122 self.assertEqual(123 self.assertEqual(
123 {124 {'subnet_mask': [message]},
124 'subnet_mask': [message],
125 'broadcast_ip': [message],
126 },
127 exception.message_dict)125 exception.message_dict)
128126
129 def test_clean_network_accepts_network_if_not_too_big(self):127 def test_clean_network_accepts_network_if_not_too_big(self):
@@ -142,7 +140,6 @@
142 network = IPNetwork('192.168.0.3/24')140 network = IPNetwork('192.168.0.3/24')
143 checked_fields = [141 checked_fields = [
144 'interface',142 'interface',
145 'broadcast_ip',
146 'subnet_mask',143 'subnet_mask',
147 'router_ip',144 'router_ip',
148 'ip_range_low',145 'ip_range_low',
@@ -160,3 +157,23 @@
160 "That field cannot be empty (unless that interface is "157 "That field cannot be empty (unless that interface is "
161 "'unmanaged')")158 "'unmanaged')")
162 self.assertEqual({field: [message]}, exception.message_dict)159 self.assertEqual({field: [message]}, exception.message_dict)
160
161 def test_clean_network_config_sets_default_if_netmask_not_given(self):
162 network = factory.getRandomNetwork()
163 nodegroup = factory.make_node_group(
164 network=network,
165 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
166 [interface] = nodegroup.get_managed_interfaces()
167 interface.full_clean()
168 self.assertEqual(unicode(network.broadcast), interface.broadcast_ip)
169
170 def test_clean_network_config_sets_no_broadcast_without_netmask(self):
171 network = factory.getRandomNetwork()
172 nodegroup = factory.make_node_group(
173 network=network,
174 management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
175 interface = NodeGroupInterface.objects.get(nodegroup=nodegroup)
176 interface.subnet_mask = None
177 interface.broadcast_ip = None
178 interface.full_clean()
179 self.assertIsNone(interface.broadcast_ip)
163180
=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
--- src/maasserver/rpc/tests/test_regionservice.py 2014-03-28 03:55:55 +0000
+++ src/maasserver/rpc/tests/test_regionservice.py 2014-03-29 17:30:47 +0000
@@ -34,6 +34,7 @@
34 RegionService,34 RegionService,
35 )35 )
36from maasserver.rpc.testing.doubles import IdentifyingRegionServer36from maasserver.rpc.testing.doubles import IdentifyingRegionServer
37from maasserver.testing.testcase import MAASServerTestCase
37from maastesting.factory import factory38from maastesting.factory import factory
38from maastesting.matchers import (39from maastesting.matchers import (
39 MockCalledOnceWith,40 MockCalledOnceWith,
@@ -195,7 +196,7 @@
195from zope.interface.verify import verifyObject196from zope.interface.verify import verifyObject
196197
197198
198class TestRegionServer(MAASTestCase):199class TestRegionServer(MAASServerTestCase):
199200
200 def test_interfaces(self):201 def test_interfaces(self):
201 protocol = RegionServer()202 protocol = RegionServer()