fix(controller): keep port links across a port-list rebuild

Node._list_ports() rebuilds every Port object, and it runs on each node
update — including every compute node.updated notification (a node
reporting started is one). The links are unaffected by that rebuild, but
the fresh port objects started out with link=None, so after any node
update the "Port is already used" guard in Link.add_node stopped matching:
a second link on an occupied port reached the compute, where the port's
NIO was replaced before the attach failed on the already-bound anchor,
leaving the link half-torn-down (on IOU, deleting the project afterwards
left the node's anchor TAPs and a per-link bridge on the host).

Carry the link back-references over by (adapter_number, port_number),
first match wins, mirroring get_port; a port that still exists keeps its
link. Regression tests pin both the reference (test_node) and the guard
(test_link: the duplicate link is refused after a node update).
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:10 +08:00
1 parent a0d76b30c0
commit 1e999d916d
3 files changed
+72

No files matched your search

+29
View File
@@ -112,6 +112,35 @@ async def test_add_node_already_connected(project, compute):
await link2.add_node(node1, 0, 4)
@pytest.mark.asyncio
async def test_add_node_already_connected_after_node_update(project, compute):
"""
A node update (any compute node.updated notification) rebuilds its port
objects; the port must still count as used afterwards, or the duplicate
link reaches the compute and fails on the already-bound NIO instead of
being refused here.
"""
project.dump = AsyncioMagicMock()
node1 = Node(project, compute, "node1", node_type="qemu")
node2 = Node(project, compute, "node2", node_type="qemu")
link = Link(project)
link.create = AsyncioMagicMock()
link.node_updated = AsyncioMagicMock()
link._project.emit_notification = MagicMock()
await link.add_node(node1, 0, 0)
await link.add_node(node2, 0, 0)
await node1.parse_node_response({"status": "started"})
link2 = Link(project)
link2.create = AsyncioMagicMock()
with pytest.raises(ControllerError, match="already used"):
await link2.add_node(node1, 0, 0)
@pytest.mark.asyncio
async def test_add_node_cloud(project, compute):
+17
View File
@@ -1191,3 +1191,20 @@ async def test_parse_node_response(node):
node.add_link(link)
await node.parse_node_response({"status": "started"})
assert link.node_updated.called
@pytest.mark.asyncio
async def test_port_link_survives_port_rebuild(node):
"""
A node update rebuilds the port objects (every compute node.updated
notification does); the link back-references must survive the rebuild,
or the "Port is already used" guard silently stops matching.
"""
port = node.get_port(0, 0)
link = MagicMock()
port.link = link
await node.parse_node_response({"status": "started"})
rebuilt = node.get_port(0, 0)
assert rebuilt is not port # the rebuild really does replace the objects
assert rebuilt.link is link