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

+26
View File
@@ -1148,6 +1148,32 @@ class Node:
return None
def _list_ports(self):
"""
Rebuild the port list, preserving the link back-references.
The list is rebuilt on every node update (a property change or a
compute ``node.updated`` notification), which creates fresh Port
objects — but the links themselves are unaffected by that rebuild,
so a port that still exists must keep pointing at its link. Without
this the "Port is already used" guard in ``Link.add_node`` silently
stops matching after any node update and a second link gets created
on an occupied port (the compute then fails on the already-bound
NIO). Links are carried over by (adapter, port), first match wins,
mirroring ``get_port``.
"""
preserved = {}
for port in self._ports or []:
if port.link is not None:
preserved.setdefault((port.adapter_number, port.port_number), port.link)
self._rebuild_ports()
for port in self._ports:
link = preserved.pop((port.adapter_number, port.port_number), None)
if link is not None and port.link is None:
port.link = link
def _rebuild_ports(self):
"""
Generate the list of port display in the client
if the compute has sent a list we return it (use by