Merge lp:~julian-edwards/maas/cluster-interfaces-as-networks into lp:~maas-committers/maas/trunk
- cluster-interfaces-as-networks
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Julian Edwards | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 2672 | ||||
Proposed branch: | lp:~julian-edwards/maas/cluster-interfaces-as-networks | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
356 lines (+180/-21) 4 files modified
src/maasserver/forms.py (+32/-0) src/maasserver/models/network.py (+16/-0) src/maasserver/models/tests/test_network.py (+39/-0) src/maasserver/tests/test_forms.py (+93/-21) |
||||
To merge this branch: | bzr merge lp:~julian-edwards/maas/cluster-interfaces-as-networks | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+230038@code.launchpad.net |
Commit message
Copy any network information from cluster interface edits into the Network table itself.
Description of the change
Pre-imped with jtv.
First part of a few branches to make a change that will list on which network each Node's MAC resides.
This simply copies network info from a cluster into the Network model itself, creating a new model as necessary, whose name is <nodegroup name>-<interface name> and the vlan_tag set as appropriate where it was present in the interface name.
At some point, we can reference the network model itself and remove the duplicate fields on the cluster interface. (I've not done it here because it would be a large change involving complex form stuff that I need to get advice on first.)
Subsequent branches will add a data migration to populate networks based on the cluster interfaces, and a web UI and API change to show the networks next to the MACs.
Julian Edwards (julian-edwards) wrote : | # |
On Friday 08 Aug 2014 05:32:16 you wrote:
> Review: Approve
>
> There are some finicky details here. I left comments inline; some of them I
> think really need addressing before landing.
Finicky is good sometimes, especially with networking! Thank you for the
thoughtful review, as ever.
> > + def save(self, *args, **kwargs):
> Can you document very briefly what unusual thing this method does? Saves
> the reader having to go through the code.
Done, well spotted.
> > + try:
> > + network.save()
> > + except ValidationError:
> > + # It already exists, keep calm and carry on.
>
> It may already exist, or you may have hit a difference between cluster
> interface validation and network validation, or there may be a bug which
> now leaves no trace. If there is no separate exception type for this, and
> you can't make an explicit call to just the uniqueness check, then at least
> log the error.
I've added the logging, thanks for calling this out.
> > + name = cluster_
> > + nodegroup_name = cluster_
> > + vlan_tag = None
> > + if '.' in name:
> > + name, vlan_tag = name.split('.', 1)
>
> Won't this generate duplicate names for e.g. eth0 and eth0.1? The form's
> blanket amnesty for ValidationErrors might hide the problem, but AFAICT one
> of the networks would simply not appear.
>
> ISTM you need to replace the dot, not strip off the vlan suffix.
Well spotted! In fact, with some more stringent testing I added later, this
actually got caught.
> > +class TestMangleNameF
> The test case's name no longer matches that of the function it tests.
Argh. I renamed the damn function with the best of intentions as well!
> > + def test_returns_
> Sometimes it does, yes. Could you make the condition under which this
> happens a bit more explicit, e.g. "straightforward name"?
Done. "Simple name".
> > + interface = sentinel.interface
> > + interface.nodegroup = sentinel.nodegroup
> > + interface.
> > + interface.interface = 'eth0'
>
> Consider extracting these 4 lines (which recur in each of these tests) into
> a helper.
Done.
> > + def test_returns_
> Maybe just "substitutes colon" instead of "returns with colon substituted"?
Done.
> All these tests use "eth0" as the base network interface. Not that that's
> particularly dangerous here, but it's a bad habit: some maniac could decide
> to take the base interface name from the operating system, or hard-code
> "eth0" for experimental changes, or assume that any colon or dot always
> comes after exactly 4 characters, without breaking the tests. There have
> to be at least some tests to show that you get the right value regardless
> of what that value actually is.
Right. I've changed the "simple" name test to use a random string for the
name.
> > +class TestNodeGroupIn
> > + """Tests for when NodeGroupInterf
>
> Given that lack of error detail ...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~julian-edwards/maas/cluster-interfaces-as-networks into lp:maas failed. Below is the output from the failed tests.
Ign http://
Hit http://
Ign http://
Hit http://
Ign http://
Hit http://
Get:1 http://
Hit http://
Hit http://
Get:2 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:3 http://
Get:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Ign http://
Ign http://
Fetched 894 kB in 0s (1,468 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'src/maasserver/forms.py' |
2 | --- src/maasserver/forms.py 2014-08-04 16:18:21 +0000 |
3 | +++ src/maasserver/forms.py 2014-08-11 06:31:29 +0000 |
4 | @@ -111,6 +111,7 @@ |
5 | Tag, |
6 | Zone, |
7 | ) |
8 | +from maasserver.models.network import get_name_and_vlan_from_cluster_interface |
9 | from maasserver.models.node import ( |
10 | fqdn_is_duplicate, |
11 | nodegroup_fqdn, |
12 | @@ -137,8 +138,11 @@ |
13 | from metadataserver.fields import Bin |
14 | from metadataserver.models import CommissioningScript |
15 | from provisioningserver.drivers.osystem import OperatingSystemRegistry |
16 | +from provisioningserver.logger import get_maas_logger |
17 | from provisioningserver.utils.network import make_network |
18 | |
19 | +maaslog = get_maas_logger() |
20 | + |
21 | # A reusable null-option for choice fields. |
22 | BLANK_CHOICE = ('', '-------') |
23 | |
24 | @@ -1176,6 +1180,34 @@ |
25 | 'static_ip_range_high', |
26 | ) |
27 | |
28 | + def save(self, *args, **kwargs): |
29 | + """Override `ModelForm`.save() so that the network data is copied |
30 | + to a `Network` instance.""" |
31 | + interface = super(NodeGroupInterfaceForm, self).save(*args, **kwargs) |
32 | + if interface.network is None: |
33 | + return interface |
34 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
35 | + network = Network( |
36 | + name=name, |
37 | + ip=unicode(interface.network.network), |
38 | + netmask=unicode(interface.network.netmask), |
39 | + vlan_tag=vlan_tag, |
40 | + # I bloody hate the damn linter. It actually prefers this. |
41 | + description="Auto created when creating interface %s on " |
42 | + "cluster %s" % ( |
43 | + interface.name, interface.nodegroup.name), |
44 | + ) |
45 | + try: |
46 | + network.save() |
47 | + except ValidationError as e: |
48 | + # It probably already exists, keep calm and carry on. |
49 | + maaslog.warning( |
50 | + "Failed to create Network when adding/editing cluster " |
51 | + "interface %s with error [%s]. This is OK if it already " |
52 | + "exists, or it could be another error" % (name, unicode(e))) |
53 | + pass |
54 | + return interface |
55 | + |
56 | def compute_name(self): |
57 | """Return the value the `name` field should have. |
58 | |
59 | |
60 | === modified file 'src/maasserver/models/network.py' |
61 | --- src/maasserver/models/network.py 2014-07-08 07:33:43 +0000 |
62 | +++ src/maasserver/models/network.py 2014-08-11 06:31:29 +0000 |
63 | @@ -186,6 +186,22 @@ |
64 | return specifier_class(spec) |
65 | |
66 | |
67 | +def get_name_and_vlan_from_cluster_interface(cluster_interface): |
68 | + """Given a `NodeGroupInterface`, return a name suitable for a `Network`. |
69 | + |
70 | + :return: a tuple of the new name and vlan tag. vlan tag may be None |
71 | + """ |
72 | + name = cluster_interface.interface |
73 | + nodegroup_name = cluster_interface.nodegroup.name |
74 | + vlan_tag = None |
75 | + if '.' in name: |
76 | + _, vlan_tag = name.split('.', 1) |
77 | + name = name.replace('.', '-') |
78 | + name = name.replace(':', '-') |
79 | + network_name = "-".join((nodegroup_name, name)) |
80 | + return network_name, vlan_tag |
81 | + |
82 | + |
83 | class NetworkManager(Manager): |
84 | """Manager for :class:`Network` model class. |
85 | |
86 | |
87 | === modified file 'src/maasserver/models/tests/test_network.py' |
88 | --- src/maasserver/models/tests/test_network.py 2014-07-16 14:12:13 +0000 |
89 | +++ src/maasserver/models/tests/test_network.py 2014-08-11 06:31:29 +0000 |
90 | @@ -22,12 +22,14 @@ |
91 | from maasserver.models.network import ( |
92 | get_specifier_type, |
93 | IPSpecifier, |
94 | + get_name_and_vlan_from_cluster_interface, |
95 | NameSpecifier, |
96 | parse_network_spec, |
97 | VLANSpecifier, |
98 | ) |
99 | from maasserver.testing.factory import factory |
100 | from maasserver.testing.testcase import MAASServerTestCase |
101 | +from mock import sentinel |
102 | from netaddr import ( |
103 | IPAddress, |
104 | IPNetwork, |
105 | @@ -129,6 +131,43 @@ |
106 | self.assertRaises(ValidationError, parse_network_spec, '10.4.4.4') |
107 | |
108 | |
109 | +class TestGetNameAndVlanFromClusterInterface(MAASServerTestCase): |
110 | + """Tests for `get_name_and_vlan_from_cluster_interface`.""" |
111 | + |
112 | + def make_interface_name(self, basename): |
113 | + interface = sentinel.interface |
114 | + interface.nodegroup = sentinel.nodegroup |
115 | + interface.nodegroup.name = factory.make_name('name') |
116 | + interface.interface = basename |
117 | + return interface |
118 | + |
119 | + def test_returns_simple_name_unaltered(self): |
120 | + interface = self.make_interface_name(factory.make_name('iface')) |
121 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
122 | + expected_name = '%s-%s' % ( |
123 | + interface.nodegroup.name, interface.interface) |
124 | + self.assertEqual((expected_name, None), (name, vlan_tag)) |
125 | + |
126 | + def test_substitutes_colon(self): |
127 | + interface = self.make_interface_name('eth0:0') |
128 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
129 | + expected_name = '%s-eth0-0' % interface.nodegroup.name |
130 | + self.assertEqual((expected_name, None), (name, vlan_tag)) |
131 | + |
132 | + def test_returns_with_vlan_tag(self): |
133 | + interface = self.make_interface_name('eth0.5') |
134 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
135 | + expected_name = '%s-eth0-5' % interface.nodegroup.name |
136 | + self.assertEqual((expected_name, '5'), (name, vlan_tag)) |
137 | + |
138 | + def test_returns_name_with_crazy_colon_and_vlan(self): |
139 | + # For truly twisted network admins. |
140 | + interface = self.make_interface_name('eth0:2.3') |
141 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
142 | + expected_name = '%s-eth0-2-3' % interface.nodegroup.name |
143 | + self.assertEqual((expected_name, '3'), (name, vlan_tag)) |
144 | + |
145 | + |
146 | class TestNetworkManager(MAASServerTestCase): |
147 | |
148 | def test_get_from_spec_validates_first(self): |
149 | |
150 | === modified file 'src/maasserver/tests/test_forms.py' |
151 | --- src/maasserver/tests/test_forms.py 2014-08-08 19:04:18 +0000 |
152 | +++ src/maasserver/tests/test_forms.py 2014-08-11 06:31:29 +0000 |
153 | @@ -16,6 +16,7 @@ |
154 | |
155 | from cStringIO import StringIO |
156 | import json |
157 | +import random |
158 | |
159 | from django import forms |
160 | from django.conf import settings |
161 | @@ -93,6 +94,7 @@ |
162 | Zone, |
163 | ) |
164 | from maasserver.models.config import DEFAULT_CONFIG |
165 | +from maasserver.models.network import get_name_and_vlan_from_cluster_interface |
166 | from maasserver.models.staticipaddress import StaticIPAddress |
167 | from maasserver.node_action import ( |
168 | Commission, |
169 | @@ -1052,23 +1054,24 @@ |
170 | ] |
171 | |
172 | |
173 | +def make_ngi_instance(nodegroup=None): |
174 | + """Create a `NodeGroupInterface` with nothing set but `nodegroup`. |
175 | + |
176 | + This is used by tests to instantiate the cluster interface form for |
177 | + a given cluster. We create an initial cluster interface object just |
178 | + to tell it which cluster that is. |
179 | + """ |
180 | + if nodegroup is None: |
181 | + nodegroup = factory.make_node_group() |
182 | + return NodeGroupInterface(nodegroup=nodegroup) |
183 | + |
184 | + |
185 | class TestNodeGroupInterfaceForm(MAASServerTestCase): |
186 | |
187 | - def make_ngi_instance(self, nodegroup=None): |
188 | - """Create a `NodeGroupInterface` with nothing set but `nodegroup`. |
189 | - |
190 | - This is used by tests to instantiate the cluster interface form for |
191 | - a given cluster. We create an initial cluster interface object just |
192 | - to tell it which cluster that is. |
193 | - """ |
194 | - if nodegroup is None: |
195 | - nodegroup = factory.make_node_group() |
196 | - return NodeGroupInterface(nodegroup=nodegroup) |
197 | - |
198 | def test__validates_parameters(self): |
199 | form = NodeGroupInterfaceForm( |
200 | data={'ip': factory.make_string()}, |
201 | - instance=self.make_ngi_instance()) |
202 | + instance=make_ngi_instance()) |
203 | self.assertFalse(form.is_valid()) |
204 | self.assertEquals( |
205 | {'ip': ['Enter a valid IPv4 or IPv6 address.']}, form._errors) |
206 | @@ -1079,7 +1082,7 @@ |
207 | for field_name in nullable_fields: |
208 | del int_settings[field_name] |
209 | form = NodeGroupInterfaceForm( |
210 | - data=int_settings, instance=self.make_ngi_instance()) |
211 | + data=int_settings, instance=make_ngi_instance()) |
212 | interface = form.save() |
213 | field_values = [ |
214 | getattr(interface, field_name) for field_name in nullable_fields] |
215 | @@ -1090,7 +1093,7 @@ |
216 | int_settings = factory.get_interface_fields() |
217 | int_settings['name'] = name |
218 | form = NodeGroupInterfaceForm( |
219 | - data=int_settings, instance=self.make_ngi_instance()) |
220 | + data=int_settings, instance=make_ngi_instance()) |
221 | interface = form.save() |
222 | self.assertEqual(name, interface.name) |
223 | |
224 | @@ -1099,7 +1102,7 @@ |
225 | int_settings['interface'] = factory.make_name('ether') |
226 | del int_settings['name'] |
227 | form = NodeGroupInterfaceForm( |
228 | - data=int_settings, instance=self.make_ngi_instance()) |
229 | + data=int_settings, instance=make_ngi_instance()) |
230 | interface = form.save() |
231 | self.assertEqual(int_settings['interface'], interface.name) |
232 | |
233 | @@ -1108,7 +1111,7 @@ |
234 | int_settings['interface'] = 'eth1+1' |
235 | del int_settings['name'] |
236 | form = NodeGroupInterfaceForm( |
237 | - data=int_settings, instance=self.make_ngi_instance()) |
238 | + data=int_settings, instance=make_ngi_instance()) |
239 | interface = form.save() |
240 | self.assertEqual('eth1--1', interface.name) |
241 | |
242 | @@ -1118,10 +1121,10 @@ |
243 | del int_settings['name'] |
244 | del int_settings['interface'] |
245 | form1 = NodeGroupInterfaceForm( |
246 | - data=int_settings, instance=self.make_ngi_instance()) |
247 | + data=int_settings, instance=make_ngi_instance()) |
248 | interface1 = form1.save() |
249 | form2 = NodeGroupInterfaceForm( |
250 | - data=int_settings, instance=self.make_ngi_instance()) |
251 | + data=int_settings, instance=make_ngi_instance()) |
252 | interface2 = form2.save() |
253 | self.assertNotIn(interface1.name, [None, '']) |
254 | self.assertNotIn(interface2.name, [None, '']) |
255 | @@ -1134,7 +1137,7 @@ |
256 | del int_settings['name'] |
257 | int_settings['interface'] = existing_interface.name |
258 | form = NodeGroupInterfaceForm( |
259 | - data=int_settings, instance=self.make_ngi_instance(cluster)) |
260 | + data=int_settings, instance=make_ngi_instance(cluster)) |
261 | interface = form.save() |
262 | self.assertThat(interface.name, StartsWith(int_settings['interface'])) |
263 | self.assertNotEqual(int_settings['interface'], interface.name) |
264 | @@ -1165,7 +1168,7 @@ |
265 | int_settings = factory.get_interface_fields( |
266 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) |
267 | form = NodeGroupInterfaceForm( |
268 | - data=int_settings, instance=self.make_ngi_instance()) |
269 | + data=int_settings, instance=make_ngi_instance()) |
270 | mock = self.patch(form, "get_duplicate_fqdns") |
271 | self.assertTrue(form.is_valid(), form.errors) |
272 | self.assertThat(mock, MockCalledOnceWith()) |
273 | @@ -1174,7 +1177,7 @@ |
274 | int_settings = factory.get_interface_fields( |
275 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) |
276 | form = NodeGroupInterfaceForm( |
277 | - data=int_settings, instance=self.make_ngi_instance()) |
278 | + data=int_settings, instance=make_ngi_instance()) |
279 | mock = self.patch(form, "get_duplicate_fqdns") |
280 | hostnames = [ |
281 | factory.make_hostname("duplicate") for _ in range(0, 3)] |
282 | @@ -1239,6 +1242,75 @@ |
283 | self.assertEqual(expected_duplicates, duplicates) |
284 | |
285 | |
286 | +class TestNodeGroupInterfaceFormNetworkCreation(MAASServerTestCase): |
287 | + """Tests for when NodeGroupInterfaceForm creates a Network.""" |
288 | + |
289 | + def test_creates_network_name(self): |
290 | + int_settings = factory.get_interface_fields() |
291 | + int_settings['interface'] = 'eth0:1' |
292 | + interface = make_ngi_instance() |
293 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
294 | + form.save() |
295 | + [network] = Network.objects.all() |
296 | + expected, _ = get_name_and_vlan_from_cluster_interface(interface) |
297 | + self.assertEqual(expected, network.name) |
298 | + |
299 | + def test_sets_vlan_tag(self): |
300 | + int_settings = factory.get_interface_fields() |
301 | + vlan_tag = random.randint(1, 10) |
302 | + int_settings['interface'] = 'eth0.%s' % vlan_tag |
303 | + interface = make_ngi_instance() |
304 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
305 | + form.save() |
306 | + [network] = Network.objects.all() |
307 | + self.assertEqual(vlan_tag, network.vlan_tag) |
308 | + |
309 | + def test_vlan_tag_is_None_if_no_vlan(self): |
310 | + int_settings = factory.get_interface_fields() |
311 | + int_settings['interface'] = 'eth0:1' |
312 | + interface = make_ngi_instance() |
313 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
314 | + form.save() |
315 | + [network] = Network.objects.all() |
316 | + self.assertIs(None, network.vlan_tag) |
317 | + |
318 | + def test_sets_network_values(self): |
319 | + int_settings = factory.get_interface_fields() |
320 | + interface = make_ngi_instance() |
321 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
322 | + form.save() |
323 | + [network] = Network.objects.all() |
324 | + expected_net_address = unicode(interface.network.network) |
325 | + expected_netmask = unicode(interface.network.netmask) |
326 | + self.assertThat( |
327 | + network, MatchesStructure.byEquality( |
328 | + ip=expected_net_address, |
329 | + netmask=expected_netmask)) |
330 | + |
331 | + def test_does_not_create_new_network_if_already_exists(self): |
332 | + int_settings = factory.get_interface_fields() |
333 | + interface = make_ngi_instance() |
334 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
335 | + # The easiest way to pre-create the same network is just to save |
336 | + # the form twice. |
337 | + form.save() |
338 | + [existing_network] = Network.objects.all() |
339 | + form.save() |
340 | + self.assertItemsEqual([existing_network], Network.objects.all()) |
341 | + |
342 | + def test_creates_many_unique_networks(self): |
343 | + names = ('eth0', 'eth0:1', 'eth0.1', 'eth0:1.2') |
344 | + for name in names: |
345 | + int_settings = factory.get_interface_fields() |
346 | + int_settings['interface'] = name |
347 | + interface = make_ngi_instance() |
348 | + form = NodeGroupInterfaceForm( |
349 | + data=int_settings, instance=interface) |
350 | + form.save() |
351 | + |
352 | + self.assertEqual(len(names), len(Network.objects.all())) |
353 | + |
354 | + |
355 | class TestValidateNewStaticIPRanges(MAASServerTestCase): |
356 | """Tests for `validate_new_static_ip_ranges`().""" |
357 |
There are some finicky details here. I left comments inline; some of them I think really need addressing before landing.