Merge lp:~julian-edwards/maas/backport-network-mac-linkage into lp:maas/1.6
- backport-network-mac-linkage
- Merge into 1.6
Status: | Merged |
---|---|
Approved by: | Julian Edwards |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2547 |
Proposed branch: | lp:~julian-edwards/maas/backport-network-mac-linkage |
Merge into: | lp:maas/1.6 |
Diff against target: |
431 lines (+249/-19) 6 files modified
src/maasserver/api.py (+19/-3) src/maasserver/forms.py (+38/-0) src/maasserver/models/network.py (+16/-0) src/maasserver/models/tests/test_network.py (+39/-0) src/maasserver/tests/test_api_nodegroup.py (+50/-13) src/maasserver/tests/test_forms.py (+87/-3) |
To merge this branch: | bzr merge lp:~julian-edwards/maas/backport-network-mac-linkage |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Julian Edwards (community) | Approve | ||
Review via email: mp+231484@code.launchpad.net |
Commit message
Backport changes from trunk that fix bug 1341619, which creates Networks from cluster interfaces and auto-links MAC addresses from nodes to them.
Description of the change
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~julian-edwards/maas/backport-network-mac-linkage into lp:maas/1.6 failed. Below is the output from the failed tests.
Ign http://
Hit http://
Ign http://
Hit http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~julian-edwards/maas/backport-network-mac-linkage into lp:maas/1.6 failed. Below is the output from the failed tests.
Ign http://
Hit http://
Hit http://
Ign http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~julian-edwards/maas/backport-network-mac-linkage into lp:maas/1.6 failed. Below is the output from the failed tests.
Ign http://
Ign http://
Hit http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Reading package lists...
sudo DEBIAN_
--
Julian Edwards (julian-edwards) wrote : | # |
FML
Preview Diff
1 | === modified file 'src/maasserver/api.py' |
2 | --- src/maasserver/api.py 2014-08-08 20:09:49 +0000 |
3 | +++ src/maasserver/api.py 2014-08-20 02:35:32 +0000 |
4 | @@ -1589,17 +1589,33 @@ |
5 | for interface in interfaces: |
6 | ip_range = netaddr.IPRange( |
7 | interface.ip_range_low, interface.ip_range_high) |
8 | - interface_ranges[interface] = ip_range |
9 | + if interface.static_ip_range_low and interface.static_ip_range_high: |
10 | + static_range = netaddr.IPRange( |
11 | + interface.static_ip_range_low, interface.static_ip_range_high) |
12 | + else: |
13 | + static_range = [] |
14 | + interface_ranges[interface] = (ip_range, static_range) |
15 | for ip, mac in leases.items(): |
16 | try: |
17 | mac_address = MACAddress.objects.get(mac_address=mac) |
18 | except MACAddress.DoesNotExist: |
19 | # Silently ignore MAC addresses that we don't know about. |
20 | continue |
21 | - for interface, ip_range in interface_ranges.items(): |
22 | - if netaddr.IPAddress(ip) in ip_range: |
23 | + for interface, (ip_range, static_range) in interface_ranges.items(): |
24 | + ipaddress = netaddr.IPAddress(ip) |
25 | + if ipaddress in ip_range or ipaddress in static_range: |
26 | mac_address.cluster_interface = interface |
27 | mac_address.save() |
28 | + |
29 | + # Locate the Network to which this MAC belongs. |
30 | + ipnetwork = interface.network |
31 | + if ipnetwork is not None: |
32 | + try: |
33 | + network = Network.objects.get(ip=ipnetwork.ip.format()) |
34 | + network.macaddress_set.add(mac_address) |
35 | + except Network.DoesNotExist: |
36 | + pass |
37 | + |
38 | # Cheap optimisation. No other interfaces will match, so |
39 | # break out of the loop. |
40 | break |
41 | |
42 | === modified file 'src/maasserver/forms.py' |
43 | --- src/maasserver/forms.py 2014-07-09 08:45:22 +0000 |
44 | +++ src/maasserver/forms.py 2014-08-20 02:35:32 +0000 |
45 | @@ -18,6 +18,7 @@ |
46 | "BootSourceForm", |
47 | "BootSourceSelectionForm", |
48 | "BulkNodeActionForm", |
49 | + "create_Network_from_NodeGroupInterface", |
50 | "CommissioningForm", |
51 | "CommissioningScriptForm", |
52 | "DownloadProgressForm", |
53 | @@ -110,6 +111,7 @@ |
54 | Tag, |
55 | Zone, |
56 | ) |
57 | +from maasserver.models.network import get_name_and_vlan_from_cluster_interface |
58 | from maasserver.models.nodegroup import NODEGROUP_CLUSTER_NAME_TEMPLATE |
59 | from maasserver.node_action import ( |
60 | ACTION_CLASSES, |
61 | @@ -1127,6 +1129,33 @@ |
62 | return True |
63 | |
64 | |
65 | +def create_Network_from_NodeGroupInterface(interface): |
66 | + """Given a `NodeGroupInterface`, create its Network counterpart.""" |
67 | + # This method cannot use non-orm model properties because it needs |
68 | + # to be used in a data migration, where they won't work. |
69 | + if not interface.subnet_mask: |
70 | + # Can be None or empty string, do nothing if so. |
71 | + return |
72 | + |
73 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
74 | + ipnetwork = make_network(interface.ip, interface.subnet_mask) |
75 | + network = Network( |
76 | + name=name, |
77 | + ip=unicode(ipnetwork.network), |
78 | + netmask=unicode(ipnetwork.netmask), |
79 | + vlan_tag=vlan_tag, |
80 | + description=( |
81 | + "Auto created when creating interface %s on cluster " |
82 | + "%s" % (interface.interface, interface.nodegroup.name)), |
83 | + ) |
84 | + try: |
85 | + network.save() |
86 | + except ValidationError: |
87 | + # It probably already exists, keep calm and carry on. |
88 | + return |
89 | + return network |
90 | + |
91 | + |
92 | class NodeGroupInterfaceForm(ModelForm): |
93 | |
94 | management = forms.TypedChoiceField( |
95 | @@ -1154,6 +1183,15 @@ |
96 | 'static_ip_range_high', |
97 | ) |
98 | |
99 | + def save(self, *args, **kwargs): |
100 | + """Override `ModelForm`.save() so that the network data is copied |
101 | + to a `Network` instance.""" |
102 | + interface = super(NodeGroupInterfaceForm, self).save(*args, **kwargs) |
103 | + if interface.network is None: |
104 | + return interface |
105 | + create_Network_from_NodeGroupInterface(interface) |
106 | + return interface |
107 | + |
108 | def clean(self): |
109 | cleaned_data = super(NodeGroupInterfaceForm, self).clean() |
110 | static_ip_range_low = cleaned_data.get('static_ip_range_low') |
111 | |
112 | === modified file 'src/maasserver/models/network.py' |
113 | --- src/maasserver/models/network.py 2014-07-08 07:33:43 +0000 |
114 | +++ src/maasserver/models/network.py 2014-08-20 02:35:32 +0000 |
115 | @@ -186,6 +186,22 @@ |
116 | return specifier_class(spec) |
117 | |
118 | |
119 | +def get_name_and_vlan_from_cluster_interface(cluster_interface): |
120 | + """Given a `NodeGroupInterface`, return a name suitable for a `Network`. |
121 | + |
122 | + :return: a tuple of the new name and vlan tag. vlan tag may be None |
123 | + """ |
124 | + name = cluster_interface.interface |
125 | + nodegroup_name = cluster_interface.nodegroup.name |
126 | + vlan_tag = None |
127 | + if '.' in name: |
128 | + _, vlan_tag = name.split('.', 1) |
129 | + name = name.replace('.', '-') |
130 | + name = name.replace(':', '-') |
131 | + network_name = "-".join((nodegroup_name, name)) |
132 | + return network_name, vlan_tag |
133 | + |
134 | + |
135 | class NetworkManager(Manager): |
136 | """Manager for :class:`Network` model class. |
137 | |
138 | |
139 | === modified file 'src/maasserver/models/tests/test_network.py' |
140 | --- src/maasserver/models/tests/test_network.py 2014-06-30 14:46:10 +0000 |
141 | +++ src/maasserver/models/tests/test_network.py 2014-08-20 02:35:32 +0000 |
142 | @@ -22,12 +22,14 @@ |
143 | from maasserver.models.network import ( |
144 | get_specifier_type, |
145 | IPSpecifier, |
146 | + get_name_and_vlan_from_cluster_interface, |
147 | NameSpecifier, |
148 | parse_network_spec, |
149 | VLANSpecifier, |
150 | ) |
151 | from maasserver.testing.factory import factory |
152 | from maasserver.testing.testcase import MAASServerTestCase |
153 | +from mock import sentinel |
154 | from netaddr import ( |
155 | IPAddress, |
156 | IPNetwork, |
157 | @@ -129,6 +131,43 @@ |
158 | self.assertRaises(ValidationError, parse_network_spec, '10.4.4.4') |
159 | |
160 | |
161 | +class TestGetNameAndVlanFromClusterInterface(MAASServerTestCase): |
162 | + """Tests for `get_name_and_vlan_from_cluster_interface`.""" |
163 | + |
164 | + def make_interface_name(self, basename): |
165 | + interface = sentinel.interface |
166 | + interface.nodegroup = sentinel.nodegroup |
167 | + interface.nodegroup.name = factory.make_name('name') |
168 | + interface.interface = basename |
169 | + return interface |
170 | + |
171 | + def test_returns_simple_name_unaltered(self): |
172 | + interface = self.make_interface_name(factory.make_name('iface')) |
173 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
174 | + expected_name = '%s-%s' % ( |
175 | + interface.nodegroup.name, interface.interface) |
176 | + self.assertEqual((expected_name, None), (name, vlan_tag)) |
177 | + |
178 | + def test_substitutes_colon(self): |
179 | + interface = self.make_interface_name('eth0:0') |
180 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
181 | + expected_name = '%s-eth0-0' % interface.nodegroup.name |
182 | + self.assertEqual((expected_name, None), (name, vlan_tag)) |
183 | + |
184 | + def test_returns_with_vlan_tag(self): |
185 | + interface = self.make_interface_name('eth0.5') |
186 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
187 | + expected_name = '%s-eth0-5' % interface.nodegroup.name |
188 | + self.assertEqual((expected_name, '5'), (name, vlan_tag)) |
189 | + |
190 | + def test_returns_name_with_crazy_colon_and_vlan(self): |
191 | + # For truly twisted network admins. |
192 | + interface = self.make_interface_name('eth0:2.3') |
193 | + name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) |
194 | + expected_name = '%s-eth0-2-3' % interface.nodegroup.name |
195 | + self.assertEqual((expected_name, '3'), (name, vlan_tag)) |
196 | + |
197 | + |
198 | class TestNetworkManager(MAASServerTestCase): |
199 | |
200 | def test_get_from_spec_validates_first(self): |
201 | |
202 | === modified file 'src/maasserver/tests/test_api_nodegroup.py' |
203 | --- src/maasserver/tests/test_api_nodegroup.py 2014-07-03 09:19:18 +0000 |
204 | +++ src/maasserver/tests/test_api_nodegroup.py 2014-08-20 02:35:32 +0000 |
205 | @@ -32,6 +32,7 @@ |
206 | NODEGROUP_STATUS_CHOICES, |
207 | NODEGROUPINTERFACE_MANAGEMENT, |
208 | ) |
209 | +from maasserver.forms import create_Network_from_NodeGroupInterface |
210 | from maasserver.models import ( |
211 | Config, |
212 | DHCPLease, |
213 | @@ -77,12 +78,16 @@ |
214 | ) |
215 | |
216 | |
217 | -def get_random_ip_from_interface_range(interface): |
218 | +def get_random_ip_from_interface_range(interface, use_static_range=None): |
219 | """Return a random IP from the pool available to an interface. |
220 | |
221 | :return: An IP address as a string.""" |
222 | - ip_range = netaddr.IPRange( |
223 | - interface.ip_range_low, interface.ip_range_high) |
224 | + if use_static_range: |
225 | + ip_range = netaddr.IPRange( |
226 | + interface.static_ip_range_low, interface.static_ip_range_high) |
227 | + else: |
228 | + ip_range = netaddr.IPRange( |
229 | + interface.ip_range_low, interface.ip_range_high) |
230 | chosen_ip = random.choice(ip_range) |
231 | return unicode(chosen_ip) |
232 | |
233 | @@ -830,7 +835,7 @@ |
234 | class TestUpdateMacClusterInterfaces(MAASServerTestCase): |
235 | """Tests for `update_mac_cluster_interfaces`().""" |
236 | |
237 | - def test_updates_mac_cluster_interfaces(self): |
238 | + def make_cluster_with_macs_and_leases(self, use_static_range=False): |
239 | cluster = factory.make_node_group() |
240 | mac_addresses = { |
241 | factory.make_mac_address(): factory.make_node_group_interface( |
242 | @@ -838,17 +843,49 @@ |
243 | for i in range(4) |
244 | } |
245 | leases = { |
246 | - get_random_ip_from_interface_range(interface): ( |
247 | + get_random_ip_from_interface_range(interface, use_static_range): ( |
248 | mac_address.mac_address) |
249 | - for mac_address, interface in mac_addresses.items() |
250 | + for mac_address, interface in mac_addresses.viewitems() |
251 | } |
252 | - update_mac_cluster_interfaces(leases, cluster) |
253 | - results = { |
254 | - mac_address: mac_address.cluster_interface |
255 | - for mac_address in MACAddress.objects.filter( |
256 | - mac_address__in=leases.values()) |
257 | - } |
258 | - self.assertEqual(mac_addresses, results) |
259 | + return cluster, mac_addresses, leases |
260 | + |
261 | + def test_updates_mac_cluster_interfaces(self): |
262 | + cluster, mac_addresses, leases = ( |
263 | + self.make_cluster_with_macs_and_leases()) |
264 | + update_mac_cluster_interfaces(leases, cluster) |
265 | + results = { |
266 | + mac_address: mac_address.cluster_interface |
267 | + for mac_address in MACAddress.objects.filter( |
268 | + mac_address__in=leases.values()) |
269 | + } |
270 | + self.assertEqual(mac_addresses, results) |
271 | + |
272 | + def test_considers_static_range_when_updating_interfaces(self): |
273 | + cluster, mac_addresses, leases = ( |
274 | + self.make_cluster_with_macs_and_leases(use_static_range=True)) |
275 | + update_mac_cluster_interfaces(leases, cluster) |
276 | + results = { |
277 | + mac_address: mac_address.cluster_interface |
278 | + for mac_address in MACAddress.objects.filter( |
279 | + mac_address__in=leases.values()) |
280 | + } |
281 | + self.assertEqual(mac_addresses, results) |
282 | + |
283 | + def test_updates_network_relations(self): |
284 | + # update_mac_cluster_interfaces should also associate the mac |
285 | + # with the network on which it resides. |
286 | + cluster, mac_addresses, leases = ( |
287 | + self.make_cluster_with_macs_and_leases()) |
288 | + expected_relations = [] |
289 | + for mac, interface in mac_addresses.viewitems(): |
290 | + net = create_Network_from_NodeGroupInterface(interface) |
291 | + expected_relations.append((net, mac)) |
292 | + update_mac_cluster_interfaces(leases, cluster) |
293 | + # Doing a single giant comparison here would be unintuitive and |
294 | + # fugly, so I'm iterating. |
295 | + for net, mac in expected_relations: |
296 | + [observed_macddress] = net.macaddress_set.all() |
297 | + self.assertThat(mac, Equals(observed_macddress)) |
298 | |
299 | def test_ignores_mac_not_attached_to_cluster(self): |
300 | cluster = factory.make_node_group() |
301 | |
302 | === modified file 'src/maasserver/tests/test_forms.py' |
303 | --- src/maasserver/tests/test_forms.py 2014-07-09 11:18:41 +0000 |
304 | +++ src/maasserver/tests/test_forms.py 2014-08-20 02:35:32 +0000 |
305 | @@ -16,6 +16,7 @@ |
306 | |
307 | from cStringIO import StringIO |
308 | import json |
309 | +import random |
310 | |
311 | from django import forms |
312 | from django.conf import settings |
313 | @@ -92,6 +93,7 @@ |
314 | Zone, |
315 | ) |
316 | from maasserver.models.config import DEFAULT_CONFIG |
317 | +from maasserver.models.network import get_name_and_vlan_from_cluster_interface |
318 | from maasserver.models.staticipaddress import StaticIPAddress |
319 | from maasserver.node_action import ( |
320 | Commission, |
321 | @@ -1054,10 +1056,24 @@ |
322 | ] |
323 | |
324 | |
325 | +def make_ngi_instance(nodegroup=None): |
326 | + """Create a `NodeGroupInterface` with nothing set but `nodegroup`. |
327 | + |
328 | + This is used by tests to instantiate the cluster interface form for |
329 | + a given cluster. We create an initial cluster interface object just |
330 | + to tell it which cluster that is. |
331 | + """ |
332 | + if nodegroup is None: |
333 | + nodegroup = factory.make_node_group() |
334 | + return NodeGroupInterface(nodegroup=nodegroup) |
335 | + |
336 | + |
337 | class TestNodeGroupInterfaceForm(MAASServerTestCase): |
338 | |
339 | def test_NodeGroupInterfaceForm_validates_parameters(self): |
340 | - form = NodeGroupInterfaceForm(data={'ip': factory.getRandomString()}) |
341 | + form = NodeGroupInterfaceForm( |
342 | + data={'ip': factory.getRandomString()}, |
343 | + instance=make_ngi_instance()) |
344 | self.assertFalse(form.is_valid()) |
345 | self.assertEquals( |
346 | {'ip': ['Enter a valid IPv4 or IPv6 address.']}, form._errors) |
347 | @@ -1069,8 +1085,7 @@ |
348 | del int_settings[field_name] |
349 | nodegroup = factory.make_node_group() |
350 | form = NodeGroupInterfaceForm( |
351 | - data=int_settings, |
352 | - instance=NodeGroupInterface(nodegroup=nodegroup)) |
353 | + data=int_settings, instance=make_ngi_instance(nodegroup)) |
354 | interface = form.save() |
355 | field_values = [ |
356 | getattr(interface, field_name) for field_name in nullable_fields] |
357 | @@ -1097,6 +1112,75 @@ |
358 | form._errors['static_ip_range_high']) |
359 | |
360 | |
361 | +class TestNodeGroupInterfaceFormNetworkCreation(MAASServerTestCase): |
362 | + """Tests for when NodeGroupInterfaceForm creates a Network.""" |
363 | + |
364 | + def test_creates_network_name(self): |
365 | + int_settings = factory.get_interface_fields() |
366 | + int_settings['interface'] = 'eth0:1' |
367 | + interface = make_ngi_instance() |
368 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
369 | + form.save() |
370 | + [network] = Network.objects.all() |
371 | + expected, _ = get_name_and_vlan_from_cluster_interface(interface) |
372 | + self.assertEqual(expected, network.name) |
373 | + |
374 | + def test_sets_vlan_tag(self): |
375 | + int_settings = factory.get_interface_fields() |
376 | + vlan_tag = random.randint(1, 10) |
377 | + int_settings['interface'] = 'eth0.%s' % vlan_tag |
378 | + interface = make_ngi_instance() |
379 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
380 | + form.save() |
381 | + [network] = Network.objects.all() |
382 | + self.assertEqual(vlan_tag, network.vlan_tag) |
383 | + |
384 | + def test_vlan_tag_is_None_if_no_vlan(self): |
385 | + int_settings = factory.get_interface_fields() |
386 | + int_settings['interface'] = 'eth0:1' |
387 | + interface = make_ngi_instance() |
388 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
389 | + form.save() |
390 | + [network] = Network.objects.all() |
391 | + self.assertIs(None, network.vlan_tag) |
392 | + |
393 | + def test_sets_network_values(self): |
394 | + int_settings = factory.get_interface_fields() |
395 | + interface = make_ngi_instance() |
396 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
397 | + form.save() |
398 | + [network] = Network.objects.all() |
399 | + expected_net_address = unicode(interface.network.network) |
400 | + expected_netmask = unicode(interface.network.netmask) |
401 | + self.assertThat( |
402 | + network, MatchesStructure.byEquality( |
403 | + ip=expected_net_address, |
404 | + netmask=expected_netmask)) |
405 | + |
406 | + def test_does_not_create_new_network_if_already_exists(self): |
407 | + int_settings = factory.get_interface_fields() |
408 | + interface = make_ngi_instance() |
409 | + form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
410 | + # The easiest way to pre-create the same network is just to save |
411 | + # the form twice. |
412 | + form.save() |
413 | + [existing_network] = Network.objects.all() |
414 | + form.save() |
415 | + self.assertItemsEqual([existing_network], Network.objects.all()) |
416 | + |
417 | + def test_creates_many_unique_networks(self): |
418 | + names = ('eth0', 'eth0:1', 'eth0.1', 'eth0:1.2') |
419 | + for name in names: |
420 | + int_settings = factory.get_interface_fields() |
421 | + int_settings['interface'] = name |
422 | + interface = make_ngi_instance() |
423 | + form = NodeGroupInterfaceForm( |
424 | + data=int_settings, instance=interface) |
425 | + form.save() |
426 | + |
427 | + self.assertEqual(len(names), len(Network.objects.all())) |
428 | + |
429 | + |
430 | class TestValidateNewStaticIPRanges(MAASServerTestCase): |
431 | """Tests for `validate_new_static_ip_ranges`().""" |
432 |
Selfing straight backport