Merge ~mpontillo/maas:interface-up-down-visited-recursion-prevent--2.4 into maas:2.4

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: 9ae8d895f877b8888596e11613499c241336ea52
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~mpontillo/maas:interface-up-down-visited-recursion-prevent--2.4
Merge into: maas:2.4
Diff against target: 116 lines (+47/-14)
2 files modified
src/maasserver/models/signals/interfaces.py (+26/-12)
src/maasserver/models/signals/tests/test_interfaces.py (+21/-2)
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+349017@code.launchpad.net

Commit message

LP: #1622105 - Make sure ensure_link_up does not cause infinite recursion.

Add visited pattern to interface_enabled_or_disabled post-save signal handler for Interface model objects, to fix an infinite recursion bug seen during end-to-end testing while interfaces were being enabled and disabled on a rack controller.

Backports: 732f0300549322a49a02c4331d6cf2cb1f86af0d

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Self-approve backport.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/models/signals/interfaces.py b/src/maasserver/models/signals/interfaces.py
index b370b05..6e9fb33 100644
--- a/src/maasserver/models/signals/interfaces.py
+++ b/src/maasserver/models/signals/interfaces.py
@@ -52,6 +52,28 @@ signals = SignalsManager()
52log = LegacyLogger()52log = LegacyLogger()
5353
5454
55class InterfaceVisitingThreadLocal(threading.local):
56 """Since infinite recursion could occur in an arbitrary interface
57 hierarchy, use thread-local storage to ensure that each interface is only
58 visited once.
59 """
60 def __init__(self):
61 super().__init__()
62 self.visiting = set()
63
64enabled_or_disabled_thread_local = InterfaceVisitingThreadLocal()
65
66
67def ensure_link_up(interface):
68 visiting = enabled_or_disabled_thread_local.visiting
69 if interface.id not in visiting:
70 try:
71 visiting.add(interface.id)
72 interface.ensure_link_up()
73 finally:
74 visiting.discard(interface.id)
75
76
55def interface_enabled_or_disabled(instance, old_values, **kwargs):77def interface_enabled_or_disabled(instance, old_values, **kwargs):
56 """When an interface is enabled be sure at minimum a LINK_UP is created.78 """When an interface is enabled be sure at minimum a LINK_UP is created.
57 When an interface is disabled make sure that all its links are removed,79 When an interface is disabled make sure that all its links are removed,
@@ -62,9 +84,10 @@ def interface_enabled_or_disabled(instance, old_values, **kwargs):
62 log.msg("%s: Physical interface enabled; ensuring link-up." % (84 log.msg("%s: Physical interface enabled; ensuring link-up." % (
63 instance.get_log_string()))85 instance.get_log_string()))
64 # Make sure it has a LINK_UP link, and for its children.86 # Make sure it has a LINK_UP link, and for its children.
65 instance.ensure_link_up()87 ensure_link_up(instance)
66 for rel in instance.children_relationships.all():88 for rel in instance.children_relationships.all():
67 rel.child.ensure_link_up()89 ensure_link_up(rel.child)
90
68 else:91 else:
69 log.msg("%s: Physical interface disabled; removing links." % (92 log.msg("%s: Physical interface disabled; removing links." % (
70 instance.get_log_string()))93 instance.get_log_string()))
@@ -157,16 +180,7 @@ for klass in INTERFACE_CLASSES:
157 klass, ['params'], delete=False)180 klass, ['params'], delete=False)
158181
159182
160class InterfaceUpdateParentsThreadLocal(threading.local):183update_parents_thread_local = InterfaceVisitingThreadLocal()
161 """Since infinite recursion could occur in an arbitrary interface
162 hierarchy, use thread-local stroage to ensure that each interface is only
163 visited once.
164 """
165 def __init__(self):
166 super().__init__()
167 self.visiting = set()
168
169update_parents_thread_local = InterfaceUpdateParentsThreadLocal()
170184
171185
172def update_interface_parents(sender, instance, created, **kwargs):186def update_interface_parents(sender, instance, created, **kwargs):
diff --git a/src/maasserver/models/signals/tests/test_interfaces.py b/src/maasserver/models/signals/tests/test_interfaces.py
index d15235b..55d7a26 100644
--- a/src/maasserver/models/signals/tests/test_interfaces.py
+++ b/src/maasserver/models/signals/tests/test_interfaces.py
@@ -14,12 +14,18 @@ from maasserver.enum import (
14 IPADDRESS_TYPE,14 IPADDRESS_TYPE,
15 NODE_TYPE,15 NODE_TYPE,
16)16)
17from maasserver.models import Controller17from maasserver.models import (
18 Controller,
19 Interface,
20)
18from maasserver.models.config import (21from maasserver.models.config import (
19 Config,22 Config,
20 NetworkDiscoveryConfig,23 NetworkDiscoveryConfig,
21)24)
22from maasserver.models.signals.interfaces import update_parents_thread_local25from maasserver.models.signals.interfaces import (
26 ensure_link_up,
27 update_parents_thread_local,
28)
23from maasserver.testing.factory import factory29from maasserver.testing.factory import factory
24from maasserver.testing.testcase import MAASServerTestCase30from maasserver.testing.testcase import MAASServerTestCase
25from maasserver.utils.orm import reload_object31from maasserver.utils.orm import reload_object
@@ -30,6 +36,11 @@ from testtools.matchers import (
30)36)
3137
3238
39def _mock_ensure_link_up(self):
40 """Mock method to test the 'visited' pattern for recursion prevention."""
41 ensure_link_up(self)
42
43
33class TestEnableAndDisableInterface(MAASServerTestCase):44class TestEnableAndDisableInterface(MAASServerTestCase):
3445
35 def test__enable_interface_creates_link_up(self):46 def test__enable_interface_creates_link_up(self):
@@ -52,6 +63,14 @@ class TestEnableAndDisableInterface(MAASServerTestCase):
52 alloc_type=IPADDRESS_TYPE.STICKY, ip=None)63 alloc_type=IPADDRESS_TYPE.STICKY, ip=None)
53 self.assertIsNotNone(link_ip)64 self.assertIsNotNone(link_ip)
5465
66 def test__ensure_link_up_only_called_once_per_interface(self):
67 interface = factory.make_Interface(
68 INTERFACE_TYPE.PHYSICAL, enabled=False)
69 interface.enabled = True
70 self.patch(Interface, 'ensure_link_up', _mock_ensure_link_up)
71 # This will cause a RecursionError if the code doesn't work.
72 interface.save()
73
55 def test__disable_interface_removes_links(self):74 def test__disable_interface_removes_links(self):
56 interface = factory.make_Interface(75 interface = factory.make_Interface(
57 INTERFACE_TYPE.PHYSICAL, enabled=True)76 INTERFACE_TYPE.PHYSICAL, enabled=True)

Subscribers

People subscribed via source and target branches