Merge branch '3.1' into fix/duplicate-device-configs-merge

Resolve conflicts against the upstream wave (PRs #2952-#2965, incl. the
typed-response/schema hardening and the drop of Python 3.10):

- controller/node.py: keep upstream's _transitioning/_confirm_status
  lifecycle guard; the kernel-anchor link wiring (node_started) runs
  after the transition completes.
- controller/link.py: keep upstream's add_node rollback (release_ports
  on create failure) and delete via release_ports; release_ports now
  resolves ports live (same staleness reason as _bind_members) and
  discards the node link unconditionally.
- schemas/controller/links.py: extend LinkFilterType with the eight
  kernel-datapath filter types and LinkFilterParameter.type with "str"
  so the newly typed /available_filters response validates; Link.filters
  stays a free-form dict — the vocabulary is capability-gated per link
  and validated by the controller (409), not by a strict request schema.
- schemas/controller/topology.py: drop the type: ignore made redundant
  by the dict filters field.
- tests: route-level filter validation now expects the controller's 409
  (unknown types, out-of-range values); packet_loss correlation is a
  valid kernel parameter (positive test added); OpenAPI assertions match
  the free-form filters property.
This commit is contained in:
YueGuobin committed 2026-10-09 10:03:50 +08:00
commit 1e0d123f4b
60 files changed
+2654 -271

No files matched your search

+34 -11
View File
@@ -399,9 +399,22 @@ class Link:
port = node.get_port(adapter_number, port_number)
if port is None:
raise ControllerNotFoundError(f"Port {adapter_number}/{port_number} for {node.name} not found")
raise ControllerNotFoundError(
f"Port {adapter_number}/{port_number} for {node.name} not found",
code="port_not_found",
details={"node_id": node.id, "adapter_number": adapter_number, "port_number": port_number},
)
if port.link is not None:
raise ControllerError("Port is already used")
raise ControllerError(
"Port is already used",
code="port_in_use",
details={
"node_id": node.id,
"adapter_number": adapter_number,
"port_number": port_number,
"link_id": port.link.id,
},
)
self._link_type = port.link_type
@@ -438,8 +451,15 @@ class Link:
{"node": node, "adapter_number": adapter_number, "port_number": port_number, "port": port, "label": label}
)
if not batch:
port.link = self
if len(self._nodes) == 2 and not batch:
await self.create()
try:
await self.create()
except BaseException:
self.release_ports()
raise
self._bind_members()
self._created = True
self._project.emit_notification("link.created", self.asdict())
@@ -447,6 +467,16 @@ class Link:
if dump:
self._project.dump()
def release_ports(self):
for n in self._nodes:
# The live port, for the same staleness reason as _bind_members;
# the link could be different from self if we rollback an
# already existing link
port = n["node"].get_port(n["adapter_number"], n["port_number"]) or n["port"]
if port.link == self:
port.link = None
n["node"].links.discard(self)
async def update_nodes(self, nodes):
for node_data in nodes:
node = self._project.get_node(node_data["node_id"])
@@ -490,14 +520,7 @@ class Link:
"""
Delete the link
"""
for n in self._nodes:
# The live port, for the same staleness reason as _bind_members;
# the link could be different from self if we rollback an
# already existing link
port = n["node"].get_port(n["adapter_number"], n["port_number"]) or n["port"]
if port.link == self:
port.link = None
n["node"].remove_link(self)
self.release_ports()
async def reset(self):
"""