Merge lp:~mpontillo/maas/fix-bridges-on-vlans-1555679 into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
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
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.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

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.

review: Needs Fixing
Revision history for this message
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?

Revision history for this message
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).

https://paste.ubuntu.com/15695629/

Revision history for this message
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.

review: Approve
Revision history for this message
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.

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/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