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

+2 -10
View File
@@ -671,11 +671,7 @@ class BaseNode:
)
# keep forwarding websocket data in both direction
if sys.version_info >= (3, 11, 0):
# Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden.
aws = [asyncio.create_task(ws_forward(telnet_writer)), asyncio.create_task(telnet_forward(telnet_reader))]
else:
aws = [ws_forward(telnet_writer), telnet_forward(telnet_reader)]
aws = [asyncio.create_task(ws_forward(telnet_writer)), asyncio.create_task(telnet_forward(telnet_reader))]
done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)
for task in done:
@@ -762,11 +758,7 @@ class BaseNode:
log.warning(f"Exception while forwarding VNC data to WebSocket: {e!r}")
# Keep forwarding WebSocket data in both directions
if sys.version_info >= (3, 11, 0):
# Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden.
aws = [asyncio.create_task(ws_forward(vnc_writer)), asyncio.create_task(vnc_forward(vnc_reader))]
else:
aws = [ws_forward(vnc_writer), vnc_forward(vnc_reader)]
aws = [asyncio.create_task(ws_forward(vnc_writer)), asyncio.create_task(vnc_forward(vnc_reader))]
done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)
for task in done:
+6 -4
View File
@@ -480,11 +480,13 @@ class Project:
stat_info = await wait_run_in_executor(lambda e=entry: e.stat())
is_dir = await wait_run_in_executor(lambda e=entry: e.is_dir())
if is_dir:
created_at: str | None = None
modified_at: str | None = None
try:
created_at = datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat()
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
created_at = modified_at = ""
created_at = modified_at = None
files.append(
{
"path": rel_path,
@@ -506,7 +508,7 @@ class Project:
created_at = datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat()
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
created_at = modified_at = ""
created_at = modified_at = None
files.append(
{
"path": rel_path,
@@ -537,7 +539,7 @@ class Project:
created_at = datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat()
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
created_at = modified_at = ""
created_at = modified_at = None
files.append(
{
"path": os.path.relpath(dir_full_path, base_path),
@@ -567,7 +569,7 @@ class Project:
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError) as e:
log.warning(f"Invalid timestamp for '{rel_path}': {e}")
created_at = modified_at = ""
created_at = modified_at = None
files.append(
{
"path": rel_path,