Merge lp:~mpontillo/maas/fix-bridges-on-vlans-1555679 into lp:~maas-committers/maas/trunk
- fix-bridges-on-vlans-1555679
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4893 |
Proposed branch: | lp:~mpontillo/maas/fix-bridges-on-vlans-1555679 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
958 lines (+541/-182) 6 files modified
src/maasserver/models/interface.py (+57/-25) src/maasserver/models/node.py (+0/-14) src/maasserver/models/signals/interfaces.py (+24/-4) src/maasserver/models/signals/tests/test_interfaces.py (+23/-5) src/maasserver/models/tests/test_interface.py (+56/-24) src/maasserver/models/tests/test_node.py (+381/-110) |
To merge this branch: | bzr merge lp:~mpontillo/maas/fix-bridges-on-vlans-1555679 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Blake Rouse (community) | Approve | ||
Review via email: mp+291307@code.launchpad.net |
Commit message
Fix bugs #1555679 and #1564657.
* Only physical interfaces (and sometimes bonds) will now be matched
by MAC address. For other interface types, it is legal for duplicates
to be found. (though it will now be logged as a warning)
* Removed a poorly-tested warning message which could sometimes trigger
in incorrect situations.
* Fix signal handling for interface save to avoid infinite recursion.
(Note: indirectly tested by rack interface update tests, since
without it, infinite recursion is seen in the tests.)
* Fix MAC validation to consider all ancestors and successors, rather
than just immediate family.
* Remove tests for bonds and bridges "moving with MAC address" (they are
now recreated if renamed, since it's not possible to uniquely identify
a bond or bridge by MAC).
* Add tests for multiple bridges with the same MAC as their parents.
Description of the change
Mike Pontillo (mpontillo) wrote : | # |
Thanks Gavin. Could you take another look at the thread local bits? I've changed it to a subclass. I was thinking about how to test this change. I feel like it's redundant since it just tests what the standard library should already ensure, and it's also tested indirectly by the rack interface update/registration code. But it doesn't stress the multi-thread aspect. Thoughts?
Mike Pontillo (mpontillo) wrote : | # |
Here's what I came up with for the test (see pastebin if you don't want to wade through the diff).
Blake Rouse (blake-rouse) wrote : | # |
Looks really good. Just one comment that I am not going to block you on, but please fix before landing.
Mike Pontillo (mpontillo) wrote : | # |
Thanks for the review. I'm going to go ahead and land this for the beta; Gavin, if you have more comments regarding the thread-local usage, I'll address them in a follow-on branch.
Gavin Panella (allenap) wrote : | # |
I think you're right to be conservative with change right now, i.e. stick with a signal handler, even if doing this in save() might be better. I have a couple of really minor comments, neither of which materially affects this landing, and a +1 for posterity.
Preview Diff
1 | === modified file 'src/maasserver/models/interface.py' |
2 | --- src/maasserver/models/interface.py 2016-04-06 05:03:18 +0000 |
3 | +++ src/maasserver/models/interface.py 2016-04-08 19:03:47 +0000 |
4 | @@ -305,10 +305,18 @@ |
5 | If the interface being created is a replacement for an interface that |
6 | already exists, the caller is responsible for deleting it. |
7 | """ |
8 | - interface = self.get_queryset().filter( |
9 | - Q(mac_address=mac_address) | Q(name=name) & Q(node=node)).first() |
10 | + if self.model.get_type() == INTERFACE_TYPE.PHYSICAL: |
11 | + # Physical interfaces have a MAC uniqueness restriction. |
12 | + interfaces = self.get_queryset().filter( |
13 | + Q(mac_address=mac_address) | Q(name=name) & Q(node=node)) |
14 | + interface = interfaces.first() |
15 | + else: |
16 | + interfaces = self.get_queryset().filter( |
17 | + Q(name=name) & Q(node=node) & Q(type=self.model.get_type())) |
18 | + interface = interfaces.first() |
19 | if interface is not None: |
20 | - if interface.type != self.model.get_type(): |
21 | + if (interface.type != self.model.get_type() and |
22 | + interface.node.id == node.id): |
23 | # This means we found the interface on this node, but the type |
24 | # didn't match what we expected. This should not happen unless |
25 | # we changed the modeling of this interface type, or the admin |
26 | @@ -927,6 +935,36 @@ |
27 | auto_ip.save() |
28 | return subnet, auto_ip |
29 | |
30 | + def get_ancestors(self): |
31 | + """Returns all the ancestors of the interface (that is, including each |
32 | + parent's parents, and so on.) |
33 | + """ |
34 | + parents = set(rel.parent for rel in self.parent_relationships.all()) |
35 | + parent_relationships = set(self.parent_relationships.all()) |
36 | + for parent_rel in parent_relationships: |
37 | + parents |= parent_rel.parent.get_ancestors() |
38 | + return parents |
39 | + |
40 | + def get_successors(self): |
41 | + """Returns all the ancestors of the interface (that is, including each |
42 | + child's children, and so on.) |
43 | + """ |
44 | + children = set(rel.child for rel in self.children_relationships.all()) |
45 | + children_relationships = set(self.children_relationships.all()) |
46 | + for child_rel in children_relationships: |
47 | + children |= child_rel.child.get_successors() |
48 | + return children |
49 | + |
50 | + def get_all_related_interfaces(self): |
51 | + """Returns all of the related interfaces (including any ancestors, |
52 | + successors, and ancestors' successors).""" |
53 | + ancestors = self.get_ancestors() |
54 | + all_related = set() |
55 | + all_related |= ancestors |
56 | + for ancestor in ancestors: |
57 | + all_related |= ancestor.get_successors() |
58 | + return all_related |
59 | + |
60 | def delete(self, remove_ip_address=True): |
61 | # We set the _skip_ip_address_removal so the signal can use it to |
62 | # skip removing the IP addresses. This is normally only done by the |
63 | @@ -1043,7 +1081,7 @@ |
64 | if len(validation_errors) > 0: |
65 | raise ValidationError(validation_errors) |
66 | |
67 | - # MAC address must be unique for all other PhysicalInterface's. |
68 | + # MAC address must be unique amongst every PhysicalInterface. |
69 | other_interfaces = PhysicalInterface.objects.filter( |
70 | mac_address=self.mac_address) |
71 | if self.id is not None: |
72 | @@ -1127,35 +1165,29 @@ |
73 | # its soon to be parents MAC address is already in use. |
74 | if self.id is not None: |
75 | interfaces = Interface.objects.filter(mac_address=self.mac_address) |
76 | - parent_ids = [ |
77 | + related_ids = [ |
78 | parent.id |
79 | - for parent in self.parents.all() |
80 | - ] |
81 | - children_ids = [ |
82 | - rel.child.id |
83 | - for rel in self.children_relationships.all() |
84 | + for parent in self.get_all_related_interfaces() |
85 | ] |
86 | bad_interfaces = [] |
87 | for interface in interfaces: |
88 | if self.id == interface.id: |
89 | # Self in database so ignore. |
90 | continue |
91 | - elif interface.id in parent_ids: |
92 | - # One of the parent MAC addresses. |
93 | - continue |
94 | - elif interface.id in children_ids: |
95 | - # One of the children MAC addresses. |
96 | + elif interface.id in related_ids: |
97 | + # Found the same MAC on either a parent, a child, or |
98 | + # another of the parents' children. |
99 | continue |
100 | else: |
101 | # Its not unique and its not a parent interface if we |
102 | # made it this far. |
103 | bad_interfaces.append(interface) |
104 | if len(bad_interfaces) > 0: |
105 | - raise ValidationError({ |
106 | - "mac_address": [ |
107 | - "This MAC address is already in use by %s." % ( |
108 | - bad_interfaces[0].node.hostname)] |
109 | - }) |
110 | + maaslog.warning( |
111 | + "While adding %s: " |
112 | + "found a MAC address already in use by %s." % ( |
113 | + self.get_log_string(), |
114 | + bad_interfaces[0].get_log_string())) |
115 | |
116 | |
117 | class BridgeInterface(ChildInterface): |
118 | @@ -1351,11 +1383,11 @@ |
119 | other_interfaces = other_interfaces.exclude(id=self.id) |
120 | other_interfaces = other_interfaces.all() |
121 | if len(other_interfaces) > 0: |
122 | - raise ValidationError({ |
123 | - "mac_address": [ |
124 | - "This MAC address is already in use by %s." % ( |
125 | - other_interfaces[0].get_log_string())] |
126 | - }) |
127 | + maaslog.warning( |
128 | + "While adding %s: " |
129 | + "found a MAC address already in use by %s." % ( |
130 | + self.get_log_string(), |
131 | + other_interfaces[0].get_log_string())) |
132 | |
133 | # Cannot have any parents. |
134 | if self.id is not None: |
135 | |
136 | === modified file 'src/maasserver/models/node.py' |
137 | --- src/maasserver/models/node.py 2016-04-06 05:03:33 +0000 |
138 | +++ src/maasserver/models/node.py 2016-04-08 19:03:47 +0000 |
139 | @@ -3243,20 +3243,6 @@ |
140 | parent_nics = Interface.objects.get_interfaces_on_node_by_name( |
141 | self, ifnames) |
142 | |
143 | - # If we didn't create the parents yet, we need to know about it, |
144 | - # because that indicates a potentially serious bug. |
145 | - if len(config["parents"]) != len(parent_nics): |
146 | - maaslog.warning( |
147 | - "Could not find all parent interfaces for {ifname} " |
148 | - "on {node_name}. Found: {modeled_interfaces}; " |
149 | - "expected: {expected_interfaces}".format( |
150 | - ifname=name, |
151 | - node_name=self.hostname, |
152 | - modeled_interfaces=[ |
153 | - iface.name for iface in parent_nics |
154 | - ].join(", "), |
155 | - expected_interfaces=ifnames.join(", "))) |
156 | - |
157 | # Ignore child interfaces that don't have parents. MAAS won't know what |
158 | # to do with them since they can't be connected to a fabric. |
159 | if len(parent_nics) == 0: |
160 | |
161 | === modified file 'src/maasserver/models/signals/interfaces.py' |
162 | --- src/maasserver/models/signals/interfaces.py 2016-03-28 13:54:47 +0000 |
163 | +++ src/maasserver/models/signals/interfaces.py 2016-04-08 19:03:47 +0000 |
164 | @@ -7,6 +7,8 @@ |
165 | "signals", |
166 | ] |
167 | |
168 | +import threading |
169 | + |
170 | from django.db.models.signals import post_save |
171 | from maasserver.enum import ( |
172 | INTERFACE_TYPE, |
173 | @@ -129,16 +131,34 @@ |
174 | interface_mtu_params_update, |
175 | klass, ['params'], delete=False) |
176 | |
177 | +thread_local = threading.local() |
178 | + |
179 | + |
180 | +class InterfaceUpdateParentsThreadLocal(threading.local): |
181 | + """Since infinite recursion could occur in an arbitrary interface |
182 | + hierarchy, use thread-local stroage to ensure that each interface is only |
183 | + visited once. |
184 | + """ |
185 | + def __init__(self): |
186 | + super().__init__() |
187 | + self.visiting = set() |
188 | + |
189 | +update_parents_thread_local = InterfaceUpdateParentsThreadLocal() |
190 | + |
191 | |
192 | def update_interface_parents(sender, instance, created, **kwargs): |
193 | """Update parents when an interface is created.""" |
194 | if instance.type in (INTERFACE_TYPE.BOND, INTERFACE_TYPE.BRIDGE): |
195 | + visiting = update_parents_thread_local.visiting |
196 | for parent in instance.parents.all(): |
197 | - # Make sure the parent has not links as well, just to be sure. |
198 | parent.clear_all_links(clearing_config=True) |
199 | - if parent.vlan != instance.vlan: |
200 | - parent.vlan = instance.vlan |
201 | - parent.save() |
202 | + if parent.vlan != instance.vlan and parent.id not in visiting: |
203 | + try: |
204 | + visiting.add(parent.id) |
205 | + parent.vlan = instance.vlan |
206 | + parent.save() |
207 | + finally: |
208 | + visiting.discard(parent.id) |
209 | |
210 | |
211 | for klass in INTERFACE_CLASSES: |
212 | |
213 | === modified file 'src/maasserver/models/signals/tests/test_interfaces.py' |
214 | --- src/maasserver/models/signals/tests/test_interfaces.py 2016-03-28 13:54:47 +0000 |
215 | +++ src/maasserver/models/signals/tests/test_interfaces.py 2016-04-08 19:03:47 +0000 |
216 | @@ -6,15 +6,21 @@ |
217 | __all__ = [] |
218 | |
219 | import random |
220 | +import threading |
221 | |
222 | from maasserver.enum import ( |
223 | INTERFACE_TYPE, |
224 | IPADDRESS_TYPE, |
225 | NODE_TYPE, |
226 | ) |
227 | +from maasserver.models.signals.interfaces import update_parents_thread_local |
228 | from maasserver.testing.factory import factory |
229 | from maasserver.testing.testcase import MAASServerTestCase |
230 | from maasserver.utils.orm import reload_object |
231 | +from testtools.matchers import ( |
232 | + Contains, |
233 | + Not, |
234 | +) |
235 | |
236 | |
237 | class TestEnableAndDisableInterface(MAASServerTestCase): |
238 | @@ -133,16 +139,16 @@ |
239 | ("bridge", {"iftype": INTERFACE_TYPE.BRIDGE}), |
240 | ) |
241 | |
242 | - def test__updates_bond_parents(self): |
243 | + def test__updates_interface_parents(self): |
244 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
245 | parent2 = factory.make_Interface( |
246 | INTERFACE_TYPE.PHYSICAL, node=parent1.node) |
247 | - bond = factory.make_Interface( |
248 | + child = factory.make_Interface( |
249 | self.iftype, parents=[parent1, parent2]) |
250 | - self.assertEqual(bond.vlan, reload_object(parent1).vlan) |
251 | - self.assertEqual(bond.vlan, reload_object(parent2).vlan) |
252 | + self.assertEqual(child.vlan, reload_object(parent1).vlan) |
253 | + self.assertEqual(child.vlan, reload_object(parent2).vlan) |
254 | |
255 | - def test__update_bond_clears_parent_links(self): |
256 | + def test__update_interface_clears_parent_links(self): |
257 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
258 | parent2 = factory.make_Interface( |
259 | INTERFACE_TYPE.PHYSICAL, node=parent1.node) |
260 | @@ -151,6 +157,18 @@ |
261 | self.iftype, parents=[parent1, parent2]) |
262 | self.assertIsNone(reload_object(static_ip)) |
263 | |
264 | + def test__visited_set_is_thread_local(self): |
265 | + thread_local = update_parents_thread_local |
266 | + thread_local.visiting.add("a") |
267 | + |
268 | + def check_a_does_not_exist(): |
269 | + self.assertThat(thread_local.visiting, Not(Contains("a"))) |
270 | + thread = threading.Thread(target=check_a_does_not_exist) |
271 | + thread.start() |
272 | + thread.join() |
273 | + self.assertThat(thread_local.visiting, Contains("a")) |
274 | + thread_local.visiting.discard("a") |
275 | + |
276 | |
277 | class TestInterfaceVLANUpdateNotController(MAASServerTestCase): |
278 | |
279 | |
280 | === modified file 'src/maasserver/models/tests/test_interface.py' |
281 | --- src/maasserver/models/tests/test_interface.py 2016-04-06 17:22:20 +0000 |
282 | +++ src/maasserver/models/tests/test_interface.py 2016-04-08 19:03:47 +0000 |
283 | @@ -13,6 +13,7 @@ |
284 | ) |
285 | from django.db import transaction |
286 | from django.http import Http404 |
287 | +from fixtures import FakeLogger |
288 | from maasserver.enum import ( |
289 | INTERFACE_LINK_TYPE, |
290 | INTERFACE_TYPE, |
291 | @@ -64,6 +65,7 @@ |
292 | ) |
293 | from testtools import ExpectedException |
294 | from testtools.matchers import ( |
295 | + Contains, |
296 | Equals, |
297 | MatchesDict, |
298 | MatchesListwise, |
299 | @@ -648,6 +650,39 @@ |
300 | node = reload_object(node) |
301 | self.assertIsNone(node.gateway_link_ipv6) |
302 | |
303 | + def test_get_ancestors_includes_grandparents(self): |
304 | + node = factory.make_Node() |
305 | + eth0 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
306 | + eth0_100 = factory.make_Interface( |
307 | + INTERFACE_TYPE.VLAN, node=node, parents=[eth0]) |
308 | + br0 = factory.make_Interface( |
309 | + INTERFACE_TYPE.BRIDGE, node=node, parents=[eth0_100]) |
310 | + self.assertThat( |
311 | + br0.get_ancestors(), Equals({eth0, eth0_100})) |
312 | + |
313 | + def test_get_successors_includes_grandchildren(self): |
314 | + node = factory.make_Node() |
315 | + eth0 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
316 | + eth0_100 = factory.make_Interface( |
317 | + INTERFACE_TYPE.VLAN, node=node, parents=[eth0]) |
318 | + br0 = factory.make_Interface( |
319 | + INTERFACE_TYPE.BRIDGE, node=node, parents=[eth0_100]) |
320 | + self.assertThat( |
321 | + eth0.get_successors(), Equals({eth0_100, br0})) |
322 | + |
323 | + def test_get_all_related_interafces_includes_all_related(self): |
324 | + node = factory.make_Node() |
325 | + eth0 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
326 | + eth0_100 = factory.make_Interface( |
327 | + INTERFACE_TYPE.VLAN, node=node, parents=[eth0]) |
328 | + eth0_101 = factory.make_Interface( |
329 | + INTERFACE_TYPE.VLAN, node=node, parents=[eth0]) |
330 | + br0 = factory.make_Interface( |
331 | + INTERFACE_TYPE.BRIDGE, node=node, parents=[eth0_100]) |
332 | + self.assertThat( |
333 | + eth0_100.get_all_related_interfaces(), Equals( |
334 | + {eth0, eth0_100, eth0_101, br0})) |
335 | + |
336 | |
337 | class PhysicalInterfaceTest(MAASServerTestCase): |
338 | |
339 | @@ -930,21 +965,19 @@ |
340 | INTERFACE_TYPE.BOND, mac_address=factory.make_mac_address(), |
341 | parents=[parent1, parent2]) |
342 | |
343 | - def test_cannot_use_none_unique_mac_address(self): |
344 | + def test_warns_for_non_unique_mac_address(self): |
345 | + logger = self.useFixture(FakeLogger("maas")) |
346 | node = factory.make_Node() |
347 | other_nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
348 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
349 | parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
350 | - # Test is that no error is raised. |
351 | - error = self.assertRaises( |
352 | - ValidationError, factory.make_Interface, |
353 | + iface = factory.make_Interface( |
354 | INTERFACE_TYPE.BOND, mac_address=other_nic.mac_address, |
355 | parents=[parent1, parent2]) |
356 | - self.assertEqual({ |
357 | - "mac_address": [ |
358 | - "This MAC address is already in use by %s." % ( |
359 | - other_nic.node.hostname)] |
360 | - }, error.message_dict) |
361 | + self.assertThat(logger.output, Contains( |
362 | + "While adding %s: " |
363 | + "found a MAC address already in use by %s." % ( |
364 | + iface.get_log_string(), other_nic.get_log_string()))) |
365 | |
366 | def test_node_is_set_to_parents_node(self): |
367 | node = factory.make_Node() |
368 | @@ -1049,21 +1082,20 @@ |
369 | INTERFACE_TYPE.BRIDGE, mac_address=factory.make_mac_address(), |
370 | parents=[parent1, parent2]) |
371 | |
372 | - def test_cannot_use_none_unique_mac_address(self): |
373 | + def test_warns_for_non_unique_mac_address(self): |
374 | + logger = self.useFixture(FakeLogger("maas")) |
375 | node = factory.make_Node() |
376 | other_nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
377 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
378 | parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
379 | # Test is that no error is raised. |
380 | - error = self.assertRaises( |
381 | - ValidationError, factory.make_Interface, |
382 | + iface = factory.make_Interface( |
383 | INTERFACE_TYPE.BRIDGE, mac_address=other_nic.mac_address, |
384 | parents=[parent1, parent2]) |
385 | - self.assertEqual({ |
386 | - "mac_address": [ |
387 | - "This MAC address is already in use by %s." % ( |
388 | - other_nic.node.hostname)] |
389 | - }, error.message_dict) |
390 | + self.assertThat(logger.output, Contains( |
391 | + "While adding %s: " |
392 | + "found a MAC address already in use by %s." % ( |
393 | + iface.get_log_string(), other_nic.get_log_string()))) |
394 | |
395 | def test_node_is_set_to_parents_node(self): |
396 | node = factory.make_Node() |
397 | @@ -1122,16 +1154,16 @@ |
398 | "node": ["This field must be blank."] |
399 | }, error.message_dict) |
400 | |
401 | - def test_mac_address_must_be_unique(self): |
402 | + def test_warns_for_non_unique_unknown_mac(self): |
403 | + logger = self.useFixture(FakeLogger("maas")) |
404 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
405 | unknown = UnknownInterface( |
406 | name="eth0", mac_address=interface.mac_address) |
407 | - error = self.assertRaises(ValidationError, unknown.save) |
408 | - self.assertEqual({ |
409 | - "mac_address": [ |
410 | - "This MAC address is already in use by %s." % ( |
411 | - interface.get_log_string())] |
412 | - }, error.message_dict) |
413 | + unknown.save() |
414 | + self.assertThat(logger.output, Contains( |
415 | + "While adding %s: " |
416 | + "found a MAC address already in use by %s." % ( |
417 | + unknown.get_log_string(), interface.get_log_string()))) |
418 | |
419 | |
420 | class UpdateIpAddressesTest(MAASServerTestCase): |
421 | |
422 | === modified file 'src/maasserver/models/tests/test_node.py' |
423 | --- src/maasserver/models/tests/test_node.py 2016-04-06 05:08:54 +0000 |
424 | +++ src/maasserver/models/tests/test_node.py 2016-04-08 19:03:47 +0000 |
425 | @@ -5579,112 +5579,6 @@ |
426 | [parent.name for parent in br0.parents.all()], |
427 | Equals(["eth0"])) |
428 | |
429 | - def test__bond_moves_bond_with_mac_address(self): |
430 | - rack = self.create_empty_rack_controller() |
431 | - fabric = factory.make_Fabric() |
432 | - vlan = fabric.get_default_vlan() |
433 | - eth0 = factory.make_Interface( |
434 | - INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan) |
435 | - eth1 = factory.make_Interface( |
436 | - INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan) |
437 | - other_node = factory.make_Node() |
438 | - other_nic = factory.make_Interface( |
439 | - INTERFACE_TYPE.PHYSICAL, node=other_node) |
440 | - bond_mac_address = factory.make_mac_address() |
441 | - bond_to_move = factory.make_Interface( |
442 | - INTERFACE_TYPE.BOND, parents=[other_nic], node=other_node, |
443 | - mac_address=bond_mac_address) |
444 | - interfaces = { |
445 | - "eth0": { |
446 | - "type": "physical", |
447 | - "mac_address": eth0.mac_address, |
448 | - "parents": [], |
449 | - "links": [], |
450 | - "enabled": True, |
451 | - }, |
452 | - "eth1": { |
453 | - "type": "physical", |
454 | - "mac_address": eth1.mac_address, |
455 | - "parents": [], |
456 | - "links": [], |
457 | - "enabled": True, |
458 | - }, |
459 | - "bond0": { |
460 | - "type": "bond", |
461 | - "mac_address": bond_mac_address, |
462 | - "parents": ["eth0", "eth1"], |
463 | - "links": [], |
464 | - "enabled": True, |
465 | - }, |
466 | - } |
467 | - rack.update_interfaces(interfaces) |
468 | - self.assertThat(rack.interface_set.count(), Equals(3)) |
469 | - self.assertThat( |
470 | - reload_object(bond_to_move), MatchesStructure.byEquality( |
471 | - type=INTERFACE_TYPE.BOND, |
472 | - name="bond0", |
473 | - mac_address=bond_mac_address, |
474 | - enabled=True, |
475 | - node=rack, |
476 | - vlan=vlan, |
477 | - )) |
478 | - self.assertThat( |
479 | - [parent.name for parent in bond_to_move.parents.all()], |
480 | - MatchesSetwise(Equals("eth0"), Equals("eth1"))) |
481 | - |
482 | - def test__bridge_moves_bridge_with_mac_address(self): |
483 | - rack = self.create_empty_rack_controller() |
484 | - fabric = factory.make_Fabric() |
485 | - vlan = fabric.get_default_vlan() |
486 | - eth0 = factory.make_Interface( |
487 | - INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan) |
488 | - eth1 = factory.make_Interface( |
489 | - INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan) |
490 | - other_node = factory.make_Node() |
491 | - other_nic = factory.make_Interface( |
492 | - INTERFACE_TYPE.PHYSICAL, node=other_node) |
493 | - bridge_mac_address = factory.make_mac_address() |
494 | - bridge_to_move = factory.make_Interface( |
495 | - INTERFACE_TYPE.BRIDGE, parents=[other_nic], node=other_node, |
496 | - mac_address=bridge_mac_address) |
497 | - interfaces = { |
498 | - "eth0": { |
499 | - "type": "physical", |
500 | - "mac_address": eth0.mac_address, |
501 | - "parents": [], |
502 | - "links": [], |
503 | - "enabled": True, |
504 | - }, |
505 | - "eth1": { |
506 | - "type": "physical", |
507 | - "mac_address": eth1.mac_address, |
508 | - "parents": [], |
509 | - "links": [], |
510 | - "enabled": True, |
511 | - }, |
512 | - "br0": { |
513 | - "type": "bridge", |
514 | - "mac_address": bridge_mac_address, |
515 | - "parents": ["eth0", "eth1"], |
516 | - "links": [], |
517 | - "enabled": True, |
518 | - }, |
519 | - } |
520 | - rack.update_interfaces(interfaces) |
521 | - self.assertThat(rack.interface_set.count(), Equals(3)) |
522 | - self.assertThat( |
523 | - reload_object(bridge_to_move), MatchesStructure.byEquality( |
524 | - type=INTERFACE_TYPE.BRIDGE, |
525 | - name="br0", |
526 | - mac_address=bridge_mac_address, |
527 | - enabled=True, |
528 | - node=rack, |
529 | - vlan=vlan, |
530 | - )) |
531 | - self.assertThat( |
532 | - [parent.name for parent in bridge_to_move.parents.all()], |
533 | - MatchesSetwise(Equals("eth0"), Equals("eth1"))) |
534 | - |
535 | def test__bond_creates_link_updates_parent_vlan(self): |
536 | rack = self.create_empty_rack_controller() |
537 | fabric = factory.make_Fabric() |
538 | @@ -5744,8 +5638,9 @@ |
539 | enabled=True, |
540 | vlan=bond0_vlan, |
541 | )) |
542 | + bond0 = get_one(Interface.objects.filter_by_ip(str(ip))) |
543 | self.assertThat( |
544 | - reload_object(bond0), MatchesStructure.byEquality( |
545 | + bond0, MatchesStructure.byEquality( |
546 | type=INTERFACE_TYPE.BOND, |
547 | name="bond0", |
548 | mac_address=bond0.mac_address, |
549 | @@ -5816,8 +5711,9 @@ |
550 | enabled=True, |
551 | vlan=br0_vlan, |
552 | )) |
553 | + br0 = get_one(Interface.objects.filter_by_ip(str(ip))) |
554 | self.assertThat( |
555 | - reload_object(br0), MatchesStructure.byEquality( |
556 | + br0, MatchesStructure.byEquality( |
557 | type=INTERFACE_TYPE.BRIDGE, |
558 | name="br0", |
559 | mac_address=br0.mac_address, |
560 | @@ -5853,7 +5749,7 @@ |
561 | eth1 = factory.make_Interface( |
562 | INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan) |
563 | bond0 = factory.make_Interface( |
564 | - INTERFACE_TYPE.BOND, parents=[eth0, eth1], vlan=vlan) |
565 | + INTERFACE_TYPE.BOND, name="bond0", parents=[eth0, eth1], vlan=vlan) |
566 | interfaces = { |
567 | "eth0": { |
568 | "type": "physical", |
569 | @@ -5884,7 +5780,7 @@ |
570 | eth1 = factory.make_Interface( |
571 | INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan) |
572 | br0 = factory.make_Interface( |
573 | - INTERFACE_TYPE.BRIDGE, parents=[eth0, eth1], vlan=vlan) |
574 | + INTERFACE_TYPE.BRIDGE, name="br0", parents=[eth0, eth1], vlan=vlan) |
575 | interfaces = { |
576 | "eth0": { |
577 | "type": "physical", |
578 | @@ -6355,6 +6251,381 @@ |
579 | r2_ens5_16 = get_one(Interface.objects.filter_by_ip("10.16.0.3")) |
580 | self.assertIsNotNone(r2_ens5_16) |
581 | |
582 | + def test__all_new_bridge_on_vlan_interface_with_identical_macs(self): |
583 | + rack = self.create_empty_rack_controller() |
584 | + default_vlan = VLAN.objects.get_default_vlan() |
585 | + br0_fabric = factory.make_Fabric() |
586 | + eth0_100_vlan = factory.make_VLAN(vid=100, fabric=br0_fabric) |
587 | + br0_subnet = factory.make_Subnet(vlan=eth0_100_vlan) |
588 | + br0_ip = factory.pick_ip_in_Subnet(br0_subnet) |
589 | + eth0_mac = factory.make_mac_address() |
590 | + br1_fabric = factory.make_Fabric() |
591 | + eth1_100_vlan = factory.make_VLAN(vid=100, fabric=br1_fabric) |
592 | + br1_subnet = factory.make_Subnet(vlan=eth1_100_vlan) |
593 | + br1_ip = factory.pick_ip_in_Subnet(br1_subnet) |
594 | + eth1_mac = factory.make_mac_address() |
595 | + eth0_101_vlan = factory.make_VLAN(vid=101, fabric=br1_fabric) |
596 | + br101_subnet = factory.make_Subnet(vlan=eth0_101_vlan) |
597 | + br101_ip = factory.pick_ip_in_Subnet(br101_subnet) |
598 | + interfaces = { |
599 | + "eth0": { |
600 | + "type": "physical", |
601 | + "mac_address": eth0_mac, |
602 | + "parents": [], |
603 | + "links": [], |
604 | + "enabled": True, |
605 | + }, |
606 | + "eth0.100": { |
607 | + "type": "vlan", |
608 | + "vid": 100, |
609 | + "mac_address": eth0_mac, |
610 | + "parents": ["eth0"], |
611 | + "links": [], |
612 | + "enabled": True, |
613 | + }, |
614 | + "eth0.101": { |
615 | + "type": "vlan", |
616 | + "vid": 101, |
617 | + "mac_address": eth0_mac, |
618 | + "parents": ["eth0"], |
619 | + "links": [], |
620 | + "enabled": True, |
621 | + }, |
622 | + "br0": { |
623 | + "type": "bridge", |
624 | + "mac_address": eth0_mac, |
625 | + "parents": ["eth0.100"], |
626 | + "links": [{ |
627 | + "mode": "static", |
628 | + "address": "%s/%d" % ( |
629 | + str(br0_ip), br0_subnet.get_ipnetwork().prefixlen), |
630 | + }], |
631 | + "enabled": True, |
632 | + }, |
633 | + "br101": { |
634 | + "type": "bridge", |
635 | + "mac_address": eth0_mac, |
636 | + "parents": ["eth0.101"], |
637 | + "links": [{ |
638 | + "mode": "static", |
639 | + "address": "%s/%d" % ( |
640 | + str(br101_ip), br101_subnet.get_ipnetwork().prefixlen), |
641 | + }], |
642 | + "enabled": True, |
643 | + }, |
644 | + "eth1": { |
645 | + "type": "physical", |
646 | + "mac_address": eth1_mac, |
647 | + "parents": [], |
648 | + "links": [], |
649 | + "enabled": True, |
650 | + }, |
651 | + "eth1.100": { |
652 | + "type": "vlan", |
653 | + "vid": 100, |
654 | + "mac_address": eth1_mac, |
655 | + "parents": ["eth0"], |
656 | + "links": [], |
657 | + "enabled": True, |
658 | + }, |
659 | + "br1": { |
660 | + "type": "bridge", |
661 | + "mac_address": eth1_mac, |
662 | + "parents": ["eth1.100"], |
663 | + "links": [{ |
664 | + "mode": "static", |
665 | + "address": "%s/%d" % ( |
666 | + str(br1_ip), br1_subnet.get_ipnetwork().prefixlen), |
667 | + }], |
668 | + "enabled": True, |
669 | + }, |
670 | + } |
671 | + rack.update_interfaces(interfaces) |
672 | + eth0 = PhysicalInterface.objects.get( |
673 | + node=rack, mac_address=interfaces["eth0"]["mac_address"]) |
674 | + self.assertThat( |
675 | + eth0, MatchesStructure.byEquality( |
676 | + type=INTERFACE_TYPE.PHYSICAL, |
677 | + name="eth0", |
678 | + mac_address=interfaces["eth0"]["mac_address"], |
679 | + enabled=True, |
680 | + vlan=default_vlan, |
681 | + )) |
682 | + eth0_100 = VLANInterface.objects.get( |
683 | + node=rack, name="eth0.100", |
684 | + mac_address=interfaces["eth0.100"]["mac_address"]) |
685 | + self.assertThat( |
686 | + eth0_100, MatchesStructure.byEquality( |
687 | + type=INTERFACE_TYPE.VLAN, |
688 | + name="eth0.100", |
689 | + mac_address=interfaces["eth0.100"]["mac_address"], |
690 | + enabled=True, |
691 | + vlan=eth0_100_vlan, |
692 | + )) |
693 | + br0 = BridgeInterface.objects.get( |
694 | + node=rack, name="br0", |
695 | + mac_address=interfaces["br0"]["mac_address"]) |
696 | + self.assertThat( |
697 | + br0, MatchesStructure.byEquality( |
698 | + type=INTERFACE_TYPE.BRIDGE, |
699 | + name="br0", |
700 | + mac_address=interfaces["br0"]["mac_address"], |
701 | + enabled=True, |
702 | + vlan=eth0_100_vlan, |
703 | + )) |
704 | + br0_addresses = list(br0.ip_addresses.all()) |
705 | + self.assertThat(br0_addresses, HasLength(1)) |
706 | + self.assertThat( |
707 | + br0_addresses[0], MatchesStructure.byEquality( |
708 | + alloc_type=IPADDRESS_TYPE.STICKY, |
709 | + ip=br0_ip, |
710 | + subnet=br0_subnet, |
711 | + )) |
712 | + br0_nic = BridgeInterface.objects.get( |
713 | + node=rack, vlan=eth0_100_vlan) |
714 | + self.assertThat( |
715 | + br0_nic, MatchesStructure.byEquality( |
716 | + type=INTERFACE_TYPE.BRIDGE, |
717 | + name="br0", |
718 | + enabled=True, |
719 | + vlan=eth0_100_vlan, |
720 | + )) |
721 | + |
722 | + def test__bridge_on_vlan_interface_with_identical_macs_replacing_phy(self): |
723 | + rack = self.create_empty_rack_controller() |
724 | + br0_fabric = factory.make_Fabric() |
725 | + eth0_100_vlan = factory.make_VLAN(vid=100, fabric=br0_fabric) |
726 | + br0_subnet = factory.make_Subnet(vlan=eth0_100_vlan) |
727 | + br0_ip = factory.pick_ip_in_Subnet(br0_subnet) |
728 | + eth0_mac = factory.make_mac_address() |
729 | + # Before the fix for bug #1555679, bridges were modeled as "physical". |
730 | + # Therefore, MAAS users needed to change the MAC of their bridge |
731 | + # interfaces, rather than using the common practice of making it the |
732 | + # same as the MAC of the parent interface. |
733 | + bogus_br0_mac = factory.make_mac_address() |
734 | + bogus_br1_mac = factory.make_mac_address() |
735 | + bogus_br101_mac = factory.make_mac_address() |
736 | + br1_fabric = factory.make_Fabric() |
737 | + eth1_100_vlan = factory.make_VLAN(vid=100, fabric=br1_fabric) |
738 | + br1_subnet = factory.make_Subnet(vlan=eth1_100_vlan) |
739 | + br1_ip = factory.pick_ip_in_Subnet(br1_subnet) |
740 | + eth1_mac = factory.make_mac_address() |
741 | + eth0_101_vlan = factory.make_VLAN(vid=101, fabric=br1_fabric) |
742 | + br101_subnet = factory.make_Subnet(vlan=eth0_101_vlan) |
743 | + br101_ip = factory.pick_ip_in_Subnet(br101_subnet) |
744 | + interfaces_old = { |
745 | + "eth0": { |
746 | + "type": "physical", |
747 | + "mac_address": eth0_mac, |
748 | + "parents": [], |
749 | + "links": [], |
750 | + "enabled": True, |
751 | + }, |
752 | + "eth0.100": { |
753 | + "type": "vlan", |
754 | + "vid": 100, |
755 | + "mac_address": eth0_mac, |
756 | + "parents": ["eth0"], |
757 | + "links": [], |
758 | + "enabled": True, |
759 | + }, |
760 | + "eth0.101": { |
761 | + "type": "vlan", |
762 | + "vid": 101, |
763 | + "mac_address": eth0_mac, |
764 | + "parents": ["eth0"], |
765 | + "links": [], |
766 | + "enabled": True, |
767 | + }, |
768 | + "br0": { |
769 | + "type": "physical", |
770 | + "mac_address": bogus_br0_mac, |
771 | + "parents": [], |
772 | + "links": [{ |
773 | + "mode": "static", |
774 | + "address": "%s/%d" % ( |
775 | + str(br0_ip), br0_subnet.get_ipnetwork().prefixlen), |
776 | + }], |
777 | + "enabled": True, |
778 | + }, |
779 | + "br101": { |
780 | + "type": "bridge", |
781 | + "mac_address": bogus_br101_mac, |
782 | + "parents": ["eth0.101"], |
783 | + "links": [{ |
784 | + "mode": "static", |
785 | + "address": "%s/%d" % ( |
786 | + str(br101_ip), br101_subnet.get_ipnetwork().prefixlen), |
787 | + }], |
788 | + "enabled": True, |
789 | + }, |
790 | + "eth1": { |
791 | + "type": "physical", |
792 | + "mac_address": eth1_mac, |
793 | + "parents": [], |
794 | + "links": [], |
795 | + "enabled": True, |
796 | + }, |
797 | + "eth1.100": { |
798 | + "type": "vlan", |
799 | + "vid": 100, |
800 | + "mac_address": eth1_mac, |
801 | + "parents": ["eth0"], |
802 | + "links": [], |
803 | + "enabled": True, |
804 | + }, |
805 | + "br1": { |
806 | + "type": "bridge", |
807 | + "mac_address": bogus_br1_mac, |
808 | + "parents": ["eth1.100"], |
809 | + "links": [{ |
810 | + "mode": "static", |
811 | + "address": "%s/%d" % ( |
812 | + str(br1_ip), br1_subnet.get_ipnetwork().prefixlen), |
813 | + }], |
814 | + "enabled": True, |
815 | + }, |
816 | + } |
817 | + rack.update_interfaces(interfaces_old) |
818 | + eth0 = PhysicalInterface.objects.get( |
819 | + node=rack, mac_address=interfaces_old["eth0"]["mac_address"]) |
820 | + self.assertThat( |
821 | + eth0, MatchesStructure.byEquality( |
822 | + type=INTERFACE_TYPE.PHYSICAL, |
823 | + name="eth0", |
824 | + mac_address=interfaces_old["eth0"]["mac_address"], |
825 | + enabled=True, |
826 | + vlan=eth0.vlan, |
827 | + )) |
828 | + # This is weird because it results in a model where eth0.100 is not |
829 | + # on the same VLAN as br0. But it's something that the admin will need |
830 | + # to fix after-the-fact, unfortunately... |
831 | + br0 = get_one(Interface.objects.filter_by_ip(br0_ip)) |
832 | + br0_vlan = br0.vlan |
833 | + interfaces = { |
834 | + "eth0": { |
835 | + "type": "physical", |
836 | + "mac_address": eth0_mac, |
837 | + "parents": [], |
838 | + "links": [], |
839 | + "enabled": True, |
840 | + }, |
841 | + "eth0.100": { |
842 | + "type": "vlan", |
843 | + "vid": 100, |
844 | + "mac_address": eth0_mac, |
845 | + "parents": ["eth0"], |
846 | + "links": [], |
847 | + "enabled": True, |
848 | + }, |
849 | + "eth0.101": { |
850 | + "type": "vlan", |
851 | + "vid": 101, |
852 | + "mac_address": eth0_mac, |
853 | + "parents": ["eth0"], |
854 | + "links": [], |
855 | + "enabled": True, |
856 | + }, |
857 | + "br0": { |
858 | + "type": "bridge", |
859 | + "mac_address": eth0_mac, |
860 | + "parents": ["eth0.100"], |
861 | + "links": [{ |
862 | + "mode": "static", |
863 | + "address": "%s/%d" % ( |
864 | + str(br0_ip), br0_subnet.get_ipnetwork().prefixlen), |
865 | + }], |
866 | + "enabled": True, |
867 | + }, |
868 | + "br101": { |
869 | + "type": "bridge", |
870 | + "mac_address": eth0_mac, |
871 | + "parents": ["eth0.101"], |
872 | + "links": [{ |
873 | + "mode": "static", |
874 | + "address": "%s/%d" % ( |
875 | + str(br101_ip), br101_subnet.get_ipnetwork().prefixlen), |
876 | + }], |
877 | + "enabled": True, |
878 | + }, |
879 | + "eth1": { |
880 | + "type": "physical", |
881 | + "mac_address": eth1_mac, |
882 | + "parents": [], |
883 | + "links": [], |
884 | + "enabled": True, |
885 | + }, |
886 | + "eth1.100": { |
887 | + "type": "vlan", |
888 | + "vid": 100, |
889 | + "mac_address": eth1_mac, |
890 | + "parents": ["eth0"], |
891 | + "links": [], |
892 | + "enabled": True, |
893 | + }, |
894 | + "br1": { |
895 | + "type": "bridge", |
896 | + "mac_address": eth1_mac, |
897 | + "parents": ["eth1.100"], |
898 | + "links": [{ |
899 | + "mode": "static", |
900 | + "address": "%s/%d" % ( |
901 | + str(br1_ip), br1_subnet.get_ipnetwork().prefixlen), |
902 | + }], |
903 | + "enabled": True, |
904 | + }, |
905 | + } |
906 | + rack.update_interfaces(interfaces) |
907 | + eth0 = PhysicalInterface.objects.get( |
908 | + node=rack, mac_address=interfaces["eth0"]["mac_address"]) |
909 | + self.assertThat( |
910 | + eth0, MatchesStructure.byEquality( |
911 | + type=INTERFACE_TYPE.PHYSICAL, |
912 | + name="eth0", |
913 | + mac_address=interfaces["eth0"]["mac_address"], |
914 | + enabled=True, |
915 | + vlan=eth0.vlan, |
916 | + )) |
917 | + eth0_100 = VLANInterface.objects.get( |
918 | + node=rack, name="eth0.100", |
919 | + mac_address=interfaces["eth0.100"]["mac_address"]) |
920 | + self.assertThat( |
921 | + eth0_100, MatchesStructure.byEquality( |
922 | + type=INTERFACE_TYPE.VLAN, |
923 | + name="eth0.100", |
924 | + mac_address=interfaces["eth0.100"]["mac_address"], |
925 | + enabled=True, |
926 | + vlan=eth0_100_vlan, |
927 | + )) |
928 | + br0 = BridgeInterface.objects.get( |
929 | + node=rack, name="br0", |
930 | + mac_address=interfaces["br0"]["mac_address"]) |
931 | + self.assertThat( |
932 | + br0, MatchesStructure.byEquality( |
933 | + type=INTERFACE_TYPE.BRIDGE, |
934 | + name="br0", |
935 | + mac_address=interfaces["br0"]["mac_address"], |
936 | + enabled=True, |
937 | + vlan=br0_vlan, |
938 | + )) |
939 | + br0_addresses = list(br0.ip_addresses.all()) |
940 | + self.assertThat(br0_addresses, HasLength(1)) |
941 | + self.assertThat( |
942 | + br0_addresses[0], MatchesStructure.byEquality( |
943 | + alloc_type=IPADDRESS_TYPE.STICKY, |
944 | + ip=br0_ip, |
945 | + subnet=br0_subnet, |
946 | + )) |
947 | + br0_nic = BridgeInterface.objects.get( |
948 | + node=rack, vlan=eth0_100_vlan) |
949 | + self.assertThat( |
950 | + br0_nic, MatchesStructure.byEquality( |
951 | + type=INTERFACE_TYPE.BRIDGE, |
952 | + name="br0", |
953 | + enabled=True, |
954 | + vlan=br0_vlan, |
955 | + )) |
956 | + |
957 | |
958 | class TestRackController(MAASServerTestCase): |
959 |
I truly don't have the experience of this part of MAAS to do a decent review, but I noticed one thing that I had to comment on.